Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions scripts/setup-git-hooks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ if (!existsSync('.git')) {
process.exit(0);
}

// Check if git is available before attempting hook setup
const gitCheck = spawnSync('git', ['--version'], { stdio: 'pipe', encoding: 'utf8' });
if (gitCheck.error || gitCheck.status !== 0) {
console.log('[setup-git-hooks] Skipping hook installation because git is not available.');
process.exit(0);
}

const desiredHooksPath = '.husky';
const currentHooksPath = getGitConfig('core.hooksPath');

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/main/web-server/WebServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('WebServer web asset resolution', () => {

it('prefers built dist/web assets over the source web index', () => {
const distWebDir = path.join(tempRoot, 'dist', 'web');
mkdirSync(distWebDir, { recursive: true });
mkdirSync(path.join(distWebDir, 'assets'), { recursive: true });
writeFileSync(
path.join(distWebDir, 'index.html'),
'<script type="module" src="./assets/main.js"></script>'
Expand Down
69 changes: 69 additions & 0 deletions src/cli/commands/cue-trigger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Cue trigger command - manually trigger a Cue subscription by name

import { withMaestroClient } from '../services/maestro-client';

interface CueTriggerOptions {
prompt?: string;
json?: boolean;
}

export async function cueTrigger(
subscriptionName: string,
options: CueTriggerOptions
): Promise<void> {
try {
const result = await withMaestroClient(async (client) => {
return client.sendCommand<{
type: string;
success: boolean;
subscriptionName: string;
error?: string;
}>(
{
type: 'trigger_cue_subscription',
subscriptionName,
prompt: options.prompt,
},
'trigger_cue_subscription_result'
);
});

if (options.json) {
console.log(
JSON.stringify({
type: 'trigger_result',
success: result.success,
subscriptionName,
...(options.prompt !== undefined ? { prompt: options.prompt } : {}),
...(result.error ? { error: result.error } : {}),
})
);
if (!result.success) {
process.exitCode = 1;
}
} else if (result.success) {
console.log(
`Triggered Cue subscription "${subscriptionName}"${options.prompt ? ' with custom prompt' : ''}`
);
} else {
console.error(
`Subscription "${subscriptionName}" not found or could not be triggered${
result.error ? `: ${result.error}` : ''
}`
);
process.exit(1);
}
Comment thread
chr1syy marked this conversation as resolved.
Comment thread
chr1syy marked this conversation as resolved.
} catch (error) {
if (options.json) {
console.log(
JSON.stringify({
type: 'error',
error: error instanceof Error ? error.message : String(error),
})
);
} else {
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
}
process.exit(1);
}
}
11 changes: 11 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { refreshFiles } from './commands/refresh-files';
import { refreshAutoRun } from './commands/refresh-auto-run';
import { status } from './commands/status';
import { autoRun } from './commands/auto-run';
import { cueTrigger } from './commands/cue-trigger';
import { settingsList } from './commands/settings-list';
import { settingsGet } from './commands/settings-get';
import { settingsSet } from './commands/settings-set';
Expand Down Expand Up @@ -161,6 +162,16 @@ program
.option('--reset-on-completion', 'Enable reset-on-completion for all documents')
.action(autoRun);

// Cue commands - interact with Maestro Cue automation
const cue = program.command('cue').description('Interact with Maestro Cue automation');

cue
.command('trigger <subscription-name>')
.description('Manually trigger a Cue subscription by name')
.option('-p, --prompt <text>', 'Override the subscription prompt with custom text')
.option('--json', 'Output as JSON (for scripting)')
.action(cueTrigger);

// Status command - check if Maestro desktop app is running and reachable
program
.command('status')
Expand Down
2 changes: 2 additions & 0 deletions src/main/cue/config/cue-config-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ export function validateCueConfigDocument(config: unknown): { valid: boolean; er
}
} else if (event === 'app.startup') {
// No additional required fields for the startup trigger.
} else if (event === 'cli.trigger') {
// No additional required fields — triggered manually via maestro-cli.
} else if (
sub.event &&
typeof sub.event === 'string' &&
Expand Down
10 changes: 6 additions & 4 deletions src/main/cue/cue-dispatch-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export interface CueDispatchService {
sub: CueSubscription,
event: CueEvent,
sourceSessionName: string,
chainDepth?: number
chainDepth?: number,
promptOverride?: string
): void;
}

Expand All @@ -32,7 +33,8 @@ export function createCueDispatchService(deps: CueDispatchServiceDeps): CueDispa
sub: CueSubscription,
event: CueEvent,
sourceSessionName: string,
chainDepth?: number
chainDepth?: number,
promptOverride?: string
): void {
if (sub.fan_out && sub.fan_out.length > 0) {
const targetNames = sub.fan_out.join(', ');
Expand Down Expand Up @@ -62,7 +64,7 @@ export function createCueDispatchService(deps: CueDispatchServiceDeps): CueDispa
// The normalizer (cue-config-normalizer.ts) resolves prompt_file → prompt
// content at config load time. sub.prompt is always a string post-normalization.
const perTargetPrompt = sub.fan_out_prompts?.[i];
const prompt = perTargetPrompt ?? sub.prompt;
const prompt = promptOverride ?? perTargetPrompt ?? sub.prompt;
if (!prompt) {
deps.onLog(
'warn',
Expand All @@ -82,7 +84,7 @@ export function createCueDispatchService(deps: CueDispatchServiceDeps): CueDispa
return;
}

const prompt = sub.prompt;
const prompt = promptOverride ?? sub.prompt;
if (!prompt) {
deps.onLog('warn', `[CUE] "${sub.name}" has no prompt — skipping dispatch`);
return;
Expand Down
21 changes: 17 additions & 4 deletions src/main/cue/cue-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,17 +352,30 @@ export class CueEngine {
* Creates a synthetic event and dispatches through the normal execution path.
* Returns true if the subscription was found and triggered.
*/
triggerSubscription(subscriptionName: string): boolean {
triggerSubscription(subscriptionName: string, promptOverride?: string): boolean {
for (const [sessionId, state] of this.registry.snapshot()) {
for (const sub of state.config.subscriptions) {
if (sub.name !== subscriptionName) continue;
if (sub.agent_id && sub.agent_id !== sessionId) continue;

const event = createCueEvent(sub.event, sub.name, { manual: true });
const event = createCueEvent(sub.event, sub.name, {
manual: true,
...(promptOverride ? { cliPrompt: promptOverride } : {}),
});

this.deps.onLog('cue', `[CUE] "${sub.name}" manually triggered`);
this.deps.onLog(
'cue',
`[CUE] "${sub.name}" manually triggered${promptOverride ? ' (with prompt override)' : ''}`
);
state.lastTriggered = event.timestamp;
this.dispatchService.dispatchSubscription(sessionId, sub, event, 'manual');
this.dispatchService.dispatchSubscription(
sessionId,
sub,
event,
'manual',
undefined,
promptOverride
);
return true;
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/main/cue/cue-template-context-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ function buildGitHubContext(event: CueEvent): Record<string, string> {
enricherRegistry.set('github.pull_request', (event) => buildGitHubContext(event));
enricherRegistry.set('github.issue', (event) => buildGitHubContext(event));

/** cli.trigger enricher — adds CLI prompt override field. */
enricherRegistry.set('cli.trigger', (event) => ({
cliPrompt: String(event.payload.cliPrompt ?? ''),
}));

// ─── Public API ──────────────────────────────────────────────────────────────

/**
Expand Down
5 changes: 3 additions & 2 deletions src/main/cue/triggers/cue-trigger-source-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ export function createTriggerSource(
return createCueGitHubPollerTriggerSource(ctx);
case 'agent.completed':
case 'app.startup':
// These are not timer/watcher-driven — the runtime handles them
// directly via the completion service / startup loop.
case 'cli.trigger':
// These are not timer/watcher-driven ��� the runtime handles them
// directly via the completion service / startup loop / CLI command.
return null;
default: {
const unsupported: never = eventType;
Expand Down
4 changes: 2 additions & 2 deletions src/main/ipc/handlers/cue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ export function registerCueHandlers(deps: CueHandlerDependencies): void {
'cue:triggerSubscription',
withIpcErrorLogging(
handlerOpts('triggerSubscription'),
async (options: { subscriptionName: string }): Promise<boolean> => {
return requireEngine().triggerSubscription(options.subscriptionName);
async (options: { subscriptionName: string; prompt?: string }): Promise<boolean> => {
return requireEngine().triggerSubscription(options.subscriptionName, options.prompt);
}
)
);
Expand Down
6 changes: 3 additions & 3 deletions src/main/preload/cue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ export function createCueApi() {
// Stop all running Cue executions
stopAll: (): Promise<void> => ipcRenderer.invoke('cue:stopAll'),

// Manually trigger a subscription by name (Run Now)
triggerSubscription: (subscriptionName: string): Promise<boolean> =>
ipcRenderer.invoke('cue:triggerSubscription', { subscriptionName }),
// Manually trigger a subscription by name (Run Now), with optional prompt override
triggerSubscription: (subscriptionName: string, prompt?: string): Promise<boolean> =>
ipcRenderer.invoke('cue:triggerSubscription', { subscriptionName, prompt }),

// Get queue status per session
getQueueStatus: (): Promise<Record<string, number>> => ipcRenderer.invoke('cue:getQueueStatus'),
Expand Down
35 changes: 35 additions & 0 deletions src/main/preload/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,41 @@ export function createProcessApi() {
ipcRenderer.send(responseChannel, result);
},

/**
* Listen for remote trigger Cue subscription requests (from web/CLI clients)
*/
onRemoteTriggerCueSubscription: (
callback: (
subscriptionName: string,
prompt: string | undefined,
responseChannel: string
) => void
): (() => void) => {
const handler = (
_: unknown,
subscriptionName: string,
prompt: string | undefined,
responseChannel: string
) => {
try {
Promise.resolve(callback(subscriptionName, prompt, responseChannel)).catch(() => {
ipcRenderer.send(responseChannel, false);
});
} catch {
ipcRenderer.send(responseChannel, false);
}
};
ipcRenderer.on('remote:triggerCueSubscription', handler);
return () => ipcRenderer.removeListener('remote:triggerCueSubscription', handler);
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Send response for remote trigger Cue subscription
*/
sendRemoteTriggerCueSubscriptionResponse: (responseChannel: string, result: unknown): void => {
ipcRenderer.send(responseChannel, result);
},

/**
* Subscribe to stderr from runCommand (separate stream)
*/
Expand Down
Loading
Loading