janissary 0.5.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/LICENSE +75 -0
- package/README.md +28 -0
- package/agent-names.json +52 -0
- package/bin/janus.mjs +27 -0
- package/chrome-extension/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
- package/chrome-extension/content-script.js +62 -0
- package/chrome-extension/manifest.json +25 -0
- package/chrome-extension/rules.json +16 -0
- package/dist/acp/index.js +93 -0
- package/dist/acp/loop.js +75 -0
- package/dist/acp/manager.js +126 -0
- package/dist/acp/rate-limit.js +8 -0
- package/dist/acp/runner.js +18 -0
- package/dist/acp/tools.js +27 -0
- package/dist/acp-loop.js +75 -0
- package/dist/acp-manager.js +109 -0
- package/dist/acp-runner.js +18 -0
- package/dist/acp.js +91 -0
- package/dist/agent/commands.js +29 -0
- package/dist/agent/communication-manager.js +71 -0
- package/dist/agent/state.js +58 -0
- package/dist/agent-commands.js +29 -0
- package/dist/agent-communication-manager.js +71 -0
- package/dist/agent-state.js +58 -0
- package/dist/app-themes.js +13 -0
- package/dist/browser/command.js +104 -0
- package/dist/browser/index.js +124 -0
- package/dist/browser/tab-helpers.js +51 -0
- package/dist/browser/tab.js +99 -0
- package/dist/browser-command.js +104 -0
- package/dist/browser-tab-helpers.js +51 -0
- package/dist/browser-tab.js +99 -0
- package/dist/browser.js +124 -0
- package/dist/buffer.js +42 -0
- package/dist/bus.js +52 -0
- package/dist/capture/manager.js +48 -0
- package/dist/capture/router.js +18 -0
- package/dist/capture-manager.js +48 -0
- package/dist/capture-router.js +20 -0
- package/dist/chrome-extension-loader.js +62 -0
- package/dist/cli-args.js +80 -0
- package/dist/command-manager.js +126 -0
- package/dist/command-router.js +14 -0
- package/dist/commands/acp-reset.js +13 -0
- package/dist/commands/acp.js +5 -0
- package/dist/commands/agent.js +5 -0
- package/dist/commands/broadcast.js +22 -0
- package/dist/commands/browser.js +5 -0
- package/dist/commands/clear.js +5 -0
- package/dist/commands/close.js +42 -0
- package/dist/commands/connection.js +5 -0
- package/dist/commands/db.js +5 -0
- package/dist/commands/edit.js +18 -0
- package/dist/commands/files.js +13 -0
- package/dist/commands/hist.js +7 -0
- package/dist/commands/index.js +62 -0
- package/dist/commands/monitor.js +61 -0
- package/dist/commands/msg.js +17 -0
- package/dist/commands/next.js +7 -0
- package/dist/commands/notifications.js +16 -0
- package/dist/commands/notify.js +20 -0
- package/dist/commands/open.js +41 -0
- package/dist/commands/profile.js +5 -0
- package/dist/commands/queue.js +37 -0
- package/dist/commands/quit.js +6 -0
- package/dist/commands/rename.js +12 -0
- package/dist/commands/resolve-target.js +12 -0
- package/dist/commands/schedule.js +74 -0
- package/dist/commands/search.js +26 -0
- package/dist/commands/send.js +51 -0
- package/dist/commands/state.js +9 -0
- package/dist/commands/syntax.js +36 -0
- package/dist/commands/tasks.js +7 -0
- package/dist/commands/theme.js +41 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands.js +70 -0
- package/dist/completion/browser.js +24 -0
- package/dist/completion/fs.js +26 -0
- package/dist/completion/handlers.js +81 -0
- package/dist/completion/helpers.js +43 -0
- package/dist/completion/index.js +43 -0
- package/dist/completion-browser.js +24 -0
- package/dist/completion-fs.js +26 -0
- package/dist/completion-handlers.js +69 -0
- package/dist/completion-helpers.js +43 -0
- package/dist/completion.js +42 -0
- package/dist/config.js +66 -0
- package/dist/connection/close.js +41 -0
- package/dist/connection/manager.js +93 -0
- package/dist/connection/parsing.js +28 -0
- package/dist/connection-close.js +41 -0
- package/dist/connection-manager.js +93 -0
- package/dist/connection-parsing.js +28 -0
- package/dist/connections.js +75 -0
- package/dist/controller-events.js +34 -0
- package/dist/controller-file-tree.js +36 -0
- package/dist/controller.js +201 -0
- package/dist/database/index.js +83 -0
- package/dist/database/manager.js +67 -0
- package/dist/database/parsing.js +54 -0
- package/dist/database/query.js +41 -0
- package/dist/database-manager.js +67 -0
- package/dist/database-parsing.js +54 -0
- package/dist/database-query.js +41 -0
- package/dist/database.js +83 -0
- package/dist/datetime.js +15 -0
- package/dist/editor/save.js +27 -0
- package/dist/editor/sync.js +10 -0
- package/dist/editor/watch-manager.js +84 -0
- package/dist/editor-save.js +24 -0
- package/dist/editor-watch-manager.js +84 -0
- package/dist/file-tree-args.js +36 -0
- package/dist/file-tree-git-refresh.js +33 -0
- package/dist/file-tree-manager.js +252 -0
- package/dist/file-tree-moves.js +26 -0
- package/dist/file-tree-open.js +56 -0
- package/dist/file-tree-rebuild.js +22 -0
- package/dist/file-tree-watch.js +27 -0
- package/dist/file-tree.js +82 -0
- package/dist/git-status.js +64 -0
- package/dist/github-token.js +19 -0
- package/dist/global-history.js +48 -0
- package/dist/harness/auto-approve.js +79 -0
- package/dist/harness/busy-status.js +91 -0
- package/dist/harness/capture-file.js +28 -0
- package/dist/harness/index.js +87 -0
- package/dist/harness/manager.js +182 -0
- package/dist/harness/models.js +8 -0
- package/dist/harness/recorder.js +89 -0
- package/dist/harness/recording-file.js +25 -0
- package/dist/harness/screen.js +79 -0
- package/dist/harness-capture-file.js +28 -0
- package/dist/harness-manager.js +112 -0
- package/dist/harness-models.js +8 -0
- package/dist/harness-screen.js +72 -0
- package/dist/harness.js +54 -0
- package/dist/index.js +163 -0
- package/dist/instance-lock.js +34 -0
- package/dist/interactive.js +33 -0
- package/dist/main.js +203 -0
- package/dist/managers.js +1 -0
- package/dist/message-handler.js +134 -0
- package/dist/messaging.js +47 -0
- package/dist/monitor/acp.js +24 -0
- package/dist/monitor/ask.js +31 -0
- package/dist/monitor/context.js +27 -0
- package/dist/monitor/editor-feed.js +49 -0
- package/dist/monitor/feed-diff.js +22 -0
- package/dist/monitor/feeds.js +19 -0
- package/dist/monitor/framing.js +29 -0
- package/dist/monitor/harness-feed.js +21 -0
- package/dist/monitor/info.js +29 -0
- package/dist/monitor/manager.js +221 -0
- package/dist/monitor/page-feed.js +21 -0
- package/dist/monitor/parsing.js +82 -0
- package/dist/monitor/reply.js +11 -0
- package/dist/monitor/session.js +36 -0
- package/dist/monitor/stop.js +33 -0
- package/dist/monitor/suggestion.js +12 -0
- package/dist/monitor/targets.js +69 -0
- package/dist/monitor/window.js +109 -0
- package/dist/monitor-acp.js +22 -0
- package/dist/monitor-info.js +18 -0
- package/dist/monitor-manager.js +233 -0
- package/dist/monitor-parsing.js +68 -0
- package/dist/monitor-session.js +19 -0
- package/dist/monitor-targets.js +37 -0
- package/dist/monitor-window.js +81 -0
- package/dist/notifications-tab.js +32 -0
- package/dist/notifications.js +87 -0
- package/dist/open-file-manager.js +100 -0
- package/dist/openers/editor.js +41 -0
- package/dist/openers/external-viewer.js +11 -0
- package/dist/openers/image.js +27 -0
- package/dist/openers/index.js +16 -0
- package/dist/openers/markdown.js +20 -0
- package/dist/openers/os-open.js +24 -0
- package/dist/openers/page.js +51 -0
- package/dist/openers/size.js +13 -0
- package/dist/openers/types.js +1 -0
- package/dist/page/sync.js +10 -0
- package/dist/paths.js +45 -0
- package/dist/persona-parsing.js +49 -0
- package/dist/personas.js +44 -0
- package/dist/profile/agent-opener.js +154 -0
- package/dist/profile/files.js +17 -0
- package/dist/profile/manager.js +72 -0
- package/dist/profile/monitors.js +23 -0
- package/dist/profile/notifications.js +11 -0
- package/dist/profile-agent-opener.js +136 -0
- package/dist/profile-manager.js +72 -0
- package/dist/profiles.js +143 -0
- package/dist/protocol.js +1 -0
- package/dist/pseudoterminal-manager.js +106 -0
- package/dist/pty.js +41 -0
- package/dist/recognizers/acp.js +38 -0
- package/dist/recognizers/analyze.js +49 -0
- package/dist/recognizers/bash.js +51 -0
- package/dist/recognizers/db.js +26 -0
- package/dist/recognizers/index.js +1 -0
- package/dist/recognizers/lexicon.js +11 -0
- package/dist/recognizers/types.js +5 -0
- package/dist/resolve.js +39 -0
- package/dist/route-choice.js +11 -0
- package/dist/sandbox/index.js +217 -0
- package/dist/sandbox/paths.js +178 -0
- package/dist/sandbox/profile.js +161 -0
- package/dist/sandbox-paths.js +159 -0
- package/dist/sandbox-profile.js +161 -0
- package/dist/sandbox.js +217 -0
- package/dist/schedule/display.js +19 -0
- package/dist/schedule/helpers.js +76 -0
- package/dist/schedule/index.js +76 -0
- package/dist/schedule/manager.js +101 -0
- package/dist/schedule/parsing.js +63 -0
- package/dist/schedule/time.js +34 -0
- package/dist/schedule-display.js +19 -0
- package/dist/schedule-helpers.js +76 -0
- package/dist/schedule-manager.js +98 -0
- package/dist/schedule-parsing.js +63 -0
- package/dist/schedule-time.js +34 -0
- package/dist/schedule.js +76 -0
- package/dist/search-matches.js +40 -0
- package/dist/security.js +44 -0
- package/dist/shell-manager.js +139 -0
- package/dist/shell.js +59 -0
- package/dist/ssh-manager.js +42 -0
- package/dist/ssh.js +39 -0
- package/dist/startup-errors.js +21 -0
- package/dist/state-event.js +14 -0
- package/dist/state-format.js +38 -0
- package/dist/syntax-themes.js +14 -0
- package/dist/tab/agent-state.js +19 -0
- package/dist/tab/cleanup.js +33 -0
- package/dist/tab/colors.js +43 -0
- package/dist/tab/creators.js +95 -0
- package/dist/tab/dock.js +35 -0
- package/dist/tab/focus-history.js +41 -0
- package/dist/tab/formatting-handlers.js +77 -0
- package/dist/tab/formatting.js +21 -0
- package/dist/tab/history.js +14 -0
- package/dist/tab/index.js +65 -0
- package/dist/tab/manager.js +329 -0
- package/dist/tab/openers.js +35 -0
- package/dist/tab/queue.js +33 -0
- package/dist/tab/rehydrate.js +15 -0
- package/dist/tab/reorder.js +15 -0
- package/dist/tab/transcript.js +15 -0
- package/dist/tab/utils.js +55 -0
- package/dist/tab/view.js +42 -0
- package/dist/tab-cleanup.js +27 -0
- package/dist/tab-colors.js +43 -0
- package/dist/tab-creators.js +85 -0
- package/dist/tab-formatting-handlers.js +77 -0
- package/dist/tab-formatting.js +21 -0
- package/dist/tab-manager.js +418 -0
- package/dist/tab-utils.js +55 -0
- package/dist/tab.js +57 -0
- package/dist/tasks.js +32 -0
- package/dist/transcript/logger.js +36 -0
- package/dist/transcript/store.js +66 -0
- package/dist/types.js +6 -0
- package/dist/user-agent.js +65 -0
- package/dist/word-wrapping.js +42 -0
- package/dist/workspace-manager.js +40 -0
- package/dist/workspace.js +120 -0
- package/help.md +97 -0
- package/package.json +138 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const MONTHS = [
|
|
2
|
+
'january', 'february', 'march', 'april', 'may', 'june',
|
|
3
|
+
'july', 'august', 'september', 'october', 'november', 'december',
|
|
4
|
+
];
|
|
5
|
+
const WEEKDAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
|
6
|
+
const SCHEDULE_USAGE = 'Usage: schedule NAME <at TIME | on DATE [at TIME] | every N(m|h|d|w) | every DAY at TIME> COMMAND | schedule list | schedule cancel <name> | schedule clear';
|
|
7
|
+
export function parseAtSchedule(tokens, now, parseTimeOfDay, fmtTime, nextOccurrenceOfTime) {
|
|
8
|
+
const tod = parseTimeOfDay(tokens[1] ?? '');
|
|
9
|
+
if (!tod)
|
|
10
|
+
return { error: `Invalid time: "${tokens[1] ?? ''}".` };
|
|
11
|
+
const command = tokens.slice(2).join(' ').trim();
|
|
12
|
+
if (!command)
|
|
13
|
+
return { error: 'No command to schedule.' };
|
|
14
|
+
return { action: 'add', entry: {
|
|
15
|
+
command, spec: `at ${fmtTime(tod)}`, recurring: false,
|
|
16
|
+
nextRun: nextOccurrenceOfTime(tod.hour, tod.minute, now),
|
|
17
|
+
} };
|
|
18
|
+
}
|
|
19
|
+
export function parseOnSchedule(tokens, now, parseMonthDay, parseTimeOfDay, fmtTime, nextDateTime) {
|
|
20
|
+
const md = parseMonthDay(tokens.slice(1));
|
|
21
|
+
if (!md)
|
|
22
|
+
return { error: 'Invalid date. Try "on august 12th" or "on 8/12".' };
|
|
23
|
+
let index = 1 + md.consumed;
|
|
24
|
+
let tod = { hour: 9, minute: 0 };
|
|
25
|
+
if (tokens[index]?.toLowerCase() === 'at') {
|
|
26
|
+
const t = parseTimeOfDay(tokens[index + 1] ?? '');
|
|
27
|
+
if (!t)
|
|
28
|
+
return { error: `Invalid time: "${tokens[index + 1] ?? ''}".` };
|
|
29
|
+
tod = t;
|
|
30
|
+
index += 2;
|
|
31
|
+
}
|
|
32
|
+
const command = tokens.slice(index).join(' ').trim();
|
|
33
|
+
if (!command)
|
|
34
|
+
return { error: 'No command to schedule.' };
|
|
35
|
+
return { action: 'add', entry: {
|
|
36
|
+
command, spec: `on ${MONTHS[md.month].slice(0, 3)} ${md.day} at ${fmtTime(tod)}`, recurring: false,
|
|
37
|
+
nextRun: nextDateTime(md.month, md.day, tod.hour, tod.minute, now),
|
|
38
|
+
} };
|
|
39
|
+
}
|
|
40
|
+
export function parseEverySchedule(tokens, now, parseInterval, parseTimeOfDay, fmtTime, nextOccurrenceOfTime, nextWeekday) {
|
|
41
|
+
const second = tokens[1] ?? '';
|
|
42
|
+
const interval = parseInterval(second);
|
|
43
|
+
if (interval !== undefined) {
|
|
44
|
+
const command = tokens.slice(2).join(' ').trim();
|
|
45
|
+
if (!command)
|
|
46
|
+
return { error: 'No command to schedule.' };
|
|
47
|
+
return { action: 'add', entry: {
|
|
48
|
+
command, spec: `every ${second.toLowerCase()}`, recurring: true,
|
|
49
|
+
intervalMs: interval, nextRun: now.getTime() + interval,
|
|
50
|
+
} };
|
|
51
|
+
}
|
|
52
|
+
const dayWord = second.toLowerCase();
|
|
53
|
+
let weekday;
|
|
54
|
+
if (dayWord !== 'day') {
|
|
55
|
+
const wd = dayWord.length >= 3 ? WEEKDAYS.findIndex((w) => w.startsWith(dayWord)) : -1;
|
|
56
|
+
if (wd < 0)
|
|
57
|
+
return { error: `Invalid interval or day: "${second}".` };
|
|
58
|
+
weekday = wd;
|
|
59
|
+
}
|
|
60
|
+
if (tokens[2]?.toLowerCase() !== 'at')
|
|
61
|
+
return { error: SCHEDULE_USAGE };
|
|
62
|
+
const tod = parseTimeOfDay(tokens[3] ?? '');
|
|
63
|
+
if (!tod)
|
|
64
|
+
return { error: `Invalid time: "${tokens[3] ?? ''}".` };
|
|
65
|
+
const command = tokens.slice(4).join(' ').trim();
|
|
66
|
+
if (!command)
|
|
67
|
+
return { error: 'No command to schedule.' };
|
|
68
|
+
const label = weekday === undefined ? 'day' : WEEKDAYS[weekday];
|
|
69
|
+
return { action: 'add', entry: {
|
|
70
|
+
command, spec: `every ${label} at ${fmtTime(tod)}`, recurring: true,
|
|
71
|
+
timeOfDay: tod, weekday,
|
|
72
|
+
nextRun: weekday === undefined
|
|
73
|
+
? nextOccurrenceOfTime(tod.hour, tod.minute, now)
|
|
74
|
+
: nextWeekday(weekday, tod.hour, tod.minute, now),
|
|
75
|
+
} };
|
|
76
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { computeNextRun, fmtNextRun } from './schedule.js';
|
|
2
|
+
import { messageBus } from './bus.js';
|
|
3
|
+
// Owns the per-tab scheduled commands (keyed by tab label) and the 1-second firing loop: at each tick
|
|
4
|
+
// it fires any entry whose next-run time has passed, reschedules recurring ones, and drops one-shots.
|
|
5
|
+
// The controller owns the tabs and persistence; this module owns the schedule state and timing.
|
|
6
|
+
export class ScheduleManager {
|
|
7
|
+
managers;
|
|
8
|
+
schedules = new Map();
|
|
9
|
+
timer;
|
|
10
|
+
constructor(managers) {
|
|
11
|
+
this.managers = managers;
|
|
12
|
+
}
|
|
13
|
+
// Begin the firing loop. `unref` so a pending tick never keeps the process alive on its own.
|
|
14
|
+
start() {
|
|
15
|
+
this.timer = setInterval(() => this.tick(), 1000);
|
|
16
|
+
this.timer.unref?.();
|
|
17
|
+
}
|
|
18
|
+
// Stop the firing loop (app shutdown).
|
|
19
|
+
stop() {
|
|
20
|
+
clearInterval(this.timer);
|
|
21
|
+
}
|
|
22
|
+
// A tab's scheduled commands, or undefined when it has none (raw — for persistence and the command
|
|
23
|
+
// context, both of which distinguish "no schedule" from "empty schedule").
|
|
24
|
+
get(label) {
|
|
25
|
+
return this.schedules.get(label);
|
|
26
|
+
}
|
|
27
|
+
// Replace a tab's scheduled commands. Persisting is the caller's concern (rehydrate/profile load
|
|
28
|
+
// restore without re-persisting; the `schedule` command persists separately).
|
|
29
|
+
set(label, entries) {
|
|
30
|
+
this.schedules.set(label, entries);
|
|
31
|
+
}
|
|
32
|
+
// Forget a tab's schedule (on tab close).
|
|
33
|
+
delete(label) {
|
|
34
|
+
this.schedules.delete(label);
|
|
35
|
+
}
|
|
36
|
+
// The schedule rows for a tab's view: id, spec, humanized next-run time, and the recurring flag.
|
|
37
|
+
view(label) {
|
|
38
|
+
return (this.schedules.get(label) ?? []).map((e) => ({
|
|
39
|
+
id: e.id, spec: e.spec, next: fmtNextRun(e.nextRun), recurring: e.recurring,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
// Fire any commands whose next-run time has passed, in every still-open tab. A recurring entry is
|
|
43
|
+
// rescheduled to its next run; a one-shot drops off. Tabs whose schedule changed are persisted
|
|
44
|
+
// (harness tabs excepted — they have no persisted agent state).
|
|
45
|
+
tick() {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
let changed = false;
|
|
48
|
+
for (const label of this.managers.tab.allLabels()) {
|
|
49
|
+
const tab = this.managers.tab.tabs.find((t) => t.label === label);
|
|
50
|
+
const sched = this.schedules.get(label);
|
|
51
|
+
if (!tab || !sched || sched.length === 0)
|
|
52
|
+
continue;
|
|
53
|
+
const remaining = this.fireDue(tab, sched, now);
|
|
54
|
+
if (!remaining)
|
|
55
|
+
continue;
|
|
56
|
+
this.schedules.set(label, remaining);
|
|
57
|
+
changed = true;
|
|
58
|
+
if (tab.view !== 'harness')
|
|
59
|
+
this.managers.tab.persist(this.managers.tab.buildAgentState(tab, { schedule: this.get(label) }));
|
|
60
|
+
}
|
|
61
|
+
if (changed)
|
|
62
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
63
|
+
}
|
|
64
|
+
// Fire one tab's due entries, returning the surviving schedule (recurring entries rescheduled,
|
|
65
|
+
// one-shots dropped), or undefined when nothing fired.
|
|
66
|
+
fireDue(tab, sched, now) {
|
|
67
|
+
let isChanged = false;
|
|
68
|
+
const remaining = [];
|
|
69
|
+
for (const e of sched) {
|
|
70
|
+
if (e.nextRun > now || !this.fire(tab, e)) {
|
|
71
|
+
remaining.push(e);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
isChanged = true;
|
|
75
|
+
if (e.recurring)
|
|
76
|
+
remaining.push({ ...e, nextRun: computeNextRun(e, new Date()) });
|
|
77
|
+
}
|
|
78
|
+
return isChanged ? remaining : undefined;
|
|
79
|
+
}
|
|
80
|
+
// Deliver a due entry to its tab: typed into a harness PTY as a line of input, or dispatched
|
|
81
|
+
// through an agent tab's command pipeline. Returns false when delivery must wait (the harness
|
|
82
|
+
// is not running), leaving the entry due so it retries on a later tick.
|
|
83
|
+
fire(tab, e) {
|
|
84
|
+
if (tab.view === 'harness') {
|
|
85
|
+
if (tab.harness?.status !== 'running' || !tab.harness.ptyId)
|
|
86
|
+
return false;
|
|
87
|
+
// Sent as one write, a long command's trailing \r can land inside the same burst the harness's
|
|
88
|
+
// own input parser treats as a paste, so it's read as inserted text rather than submit. Splitting
|
|
89
|
+
// the \r into its own write after the text has been processed mimics organic typing and avoids that.
|
|
90
|
+
const ptyId = tab.harness.ptyId;
|
|
91
|
+
this.managers.pty.input(ptyId, e.command);
|
|
92
|
+
setTimeout(() => this.managers.pty.input(ptyId, '\r'), 50);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
this.managers.command.dispatchTo(tab.label, `${e.command} ## scheduled ##`);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const MONTHS = [
|
|
2
|
+
'january', 'february', 'march', 'april', 'may', 'june',
|
|
3
|
+
'july', 'august', 'september', 'october', 'november', 'december',
|
|
4
|
+
];
|
|
5
|
+
const UNIT_MS = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
|
|
6
|
+
export { MONTHS };
|
|
7
|
+
export function parseTimeOfDay(tok) {
|
|
8
|
+
// Anchored at both ends; bounded quantifiers only — not a ReDoS risk.
|
|
9
|
+
// eslint-disable-next-line security/detect-unsafe-regex
|
|
10
|
+
const m = /^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/i.exec(tok.trim());
|
|
11
|
+
if (!m)
|
|
12
|
+
return undefined;
|
|
13
|
+
let hour = Number(m[1]);
|
|
14
|
+
const minute = m[2] ? Number(m[2]) : 0;
|
|
15
|
+
const ap = m[3]?.toLowerCase();
|
|
16
|
+
if (ap) {
|
|
17
|
+
if (hour < 1 || hour > 12)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (ap === 'pm' && hour !== 12)
|
|
20
|
+
hour += 12;
|
|
21
|
+
if (ap === 'am' && hour === 12)
|
|
22
|
+
hour = 0;
|
|
23
|
+
}
|
|
24
|
+
else if (hour > 23) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
if (minute > 59)
|
|
28
|
+
return undefined;
|
|
29
|
+
return { hour, minute };
|
|
30
|
+
}
|
|
31
|
+
export function parseInterval(tok) {
|
|
32
|
+
const m = /^(\d+)(m|h|d|w)$/i.exec(tok.trim());
|
|
33
|
+
if (!m)
|
|
34
|
+
return undefined;
|
|
35
|
+
const n = Number(m[1]);
|
|
36
|
+
if (n <= 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
return n * UNIT_MS[m[2].toLowerCase()];
|
|
39
|
+
}
|
|
40
|
+
export function parseMonthDay(tokens) {
|
|
41
|
+
const first = tokens[0];
|
|
42
|
+
if (!first)
|
|
43
|
+
return undefined;
|
|
44
|
+
const slash = /^(\d{1,2})\/(\d{1,2})$/.exec(first);
|
|
45
|
+
if (slash) {
|
|
46
|
+
const month = Number(slash[1]) - 1;
|
|
47
|
+
const day = Number(slash[2]);
|
|
48
|
+
if (month < 0 || month > 11 || day < 1 || day > 31)
|
|
49
|
+
return undefined;
|
|
50
|
+
return { month, day, consumed: 1 };
|
|
51
|
+
}
|
|
52
|
+
const lc = first.toLowerCase();
|
|
53
|
+
const month = lc.length >= 3 ? MONTHS.findIndex((m) => m.startsWith(lc)) : -1;
|
|
54
|
+
if (month < 0)
|
|
55
|
+
return undefined;
|
|
56
|
+
const dm = /^(\d{1,2})(?:st|nd|rd|th)?$/i.exec(tokens[1] ?? '');
|
|
57
|
+
if (!dm)
|
|
58
|
+
return undefined;
|
|
59
|
+
const day = Number(dm[1]);
|
|
60
|
+
if (day < 1 || day > 31)
|
|
61
|
+
return undefined;
|
|
62
|
+
return { month, day, consumed: 2 };
|
|
63
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function nextOccurrenceOfTime(hour, minute, now) {
|
|
2
|
+
const d = new Date(now);
|
|
3
|
+
d.setHours(hour, minute, 0, 0);
|
|
4
|
+
if (d.getTime() <= now.getTime())
|
|
5
|
+
d.setDate(d.getDate() + 1);
|
|
6
|
+
return d.getTime();
|
|
7
|
+
}
|
|
8
|
+
export function nextWeekday(weekday, hour, minute, now) {
|
|
9
|
+
const d = new Date(now);
|
|
10
|
+
d.setHours(hour, minute, 0, 0);
|
|
11
|
+
let diff = (weekday - d.getDay() + 7) % 7;
|
|
12
|
+
if (diff === 0 && d.getTime() <= now.getTime())
|
|
13
|
+
diff = 7;
|
|
14
|
+
d.setDate(d.getDate() + diff);
|
|
15
|
+
return d.getTime();
|
|
16
|
+
}
|
|
17
|
+
export function nextDateTime(month, day, hour, minute, now) {
|
|
18
|
+
const year = now.getFullYear();
|
|
19
|
+
const d = new Date(year, month, day, hour, minute, 0, 0);
|
|
20
|
+
if (d.getTime() <= now.getTime())
|
|
21
|
+
d.setFullYear(year + 1);
|
|
22
|
+
return d.getTime();
|
|
23
|
+
}
|
|
24
|
+
export function computeNextRun(entry, now) {
|
|
25
|
+
if (entry.intervalMs)
|
|
26
|
+
return now.getTime() + entry.intervalMs;
|
|
27
|
+
if (entry.timeOfDay) {
|
|
28
|
+
const { hour, minute } = entry.timeOfDay;
|
|
29
|
+
return entry.weekday === undefined
|
|
30
|
+
? nextOccurrenceOfTime(hour, minute, now)
|
|
31
|
+
: nextWeekday(entry.weekday, hour, minute, now);
|
|
32
|
+
}
|
|
33
|
+
return now.getTime();
|
|
34
|
+
}
|
package/dist/schedule.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Parsing and next-run math for the `schedule` command. Pure (no I/O) so it is fully
|
|
2
|
+
// unit-testable; callers (the command + the scheduler tick) own the side effects.
|
|
3
|
+
import { parseAtSchedule, parseOnSchedule, parseEverySchedule } from './schedule-helpers.js';
|
|
4
|
+
import { parseTimeOfDay, parseInterval, parseMonthDay } from './schedule-parsing.js';
|
|
5
|
+
export { parseTimeOfDay, parseInterval, parseMonthDay } from './schedule-parsing.js';
|
|
6
|
+
import { nextOccurrenceOfTime, nextWeekday, nextDateTime } from './schedule-time.js';
|
|
7
|
+
export { nextOccurrenceOfTime, nextWeekday, computeNextRun } from './schedule-time.js';
|
|
8
|
+
import { fmtTime } from './schedule-display.js';
|
|
9
|
+
export { fmtNextRun, formatSchedule } from './schedule-display.js';
|
|
10
|
+
export const SCHEDULE_USAGE = 'Usage: schedule NAME [in TAB] <at TIME | on DATE [at TIME] | every N(m|h|d|w) | every DAY at TIME> COMMAND'
|
|
11
|
+
+ ' | schedule list [in TAB] | schedule cancel <name> [in TAB] | schedule clear [in TAB]';
|
|
12
|
+
// Parse a trailing `in <tab>` clause starting at `tokens[index]`. Returns the target label,
|
|
13
|
+
// an empty object when the clause is absent, or an error when it is malformed or followed
|
|
14
|
+
// by extra tokens (the clause must end the command).
|
|
15
|
+
function parseInClause(tokens, index) {
|
|
16
|
+
const [keyword, target, ...extra] = tokens.slice(index);
|
|
17
|
+
if (keyword === undefined)
|
|
18
|
+
return {};
|
|
19
|
+
if (keyword.toLowerCase() !== 'in' || target === undefined || extra.length > 0)
|
|
20
|
+
return { error: SCHEDULE_USAGE };
|
|
21
|
+
return { target };
|
|
22
|
+
}
|
|
23
|
+
export function parseScheduleCommand(rest, now) {
|
|
24
|
+
const trimmed = rest.trim();
|
|
25
|
+
if (!trimmed)
|
|
26
|
+
return { error: SCHEDULE_USAGE };
|
|
27
|
+
const tokens = trimmed.split(/\s+/);
|
|
28
|
+
const head = tokens[0].toLowerCase();
|
|
29
|
+
if (head === 'list' || head === 'clear') {
|
|
30
|
+
const clause = parseInClause(tokens, 1);
|
|
31
|
+
if ('error' in clause)
|
|
32
|
+
return clause;
|
|
33
|
+
return { action: head, ...clause };
|
|
34
|
+
}
|
|
35
|
+
if (head === 'cancel') {
|
|
36
|
+
if (!tokens[1])
|
|
37
|
+
return { error: 'Usage: schedule cancel <name> [in TAB]' };
|
|
38
|
+
const clause = parseInClause(tokens, 2);
|
|
39
|
+
if ('error' in clause)
|
|
40
|
+
return clause;
|
|
41
|
+
return { action: 'cancel', id: tokens[1], ...clause };
|
|
42
|
+
}
|
|
43
|
+
// Otherwise the first token names the timer (becoming its id, shown in the schedule window
|
|
44
|
+
// and used by `schedule cancel <name>`), an optional `in <tab>` picks the tab the timer
|
|
45
|
+
// belongs to, and the remainder is the schedule form.
|
|
46
|
+
const name = tokens[0];
|
|
47
|
+
let bodyTokens = tokens.slice(1);
|
|
48
|
+
let target;
|
|
49
|
+
if (bodyTokens[0]?.toLowerCase() === 'in') {
|
|
50
|
+
if (!bodyTokens[1])
|
|
51
|
+
return { error: SCHEDULE_USAGE };
|
|
52
|
+
target = bodyTokens[1];
|
|
53
|
+
bodyTokens = bodyTokens.slice(2);
|
|
54
|
+
}
|
|
55
|
+
const body = parseScheduleBody(bodyTokens.join(' '), now);
|
|
56
|
+
if ('error' in body)
|
|
57
|
+
return body;
|
|
58
|
+
return { ...body, name, ...(target !== undefined && { target }) };
|
|
59
|
+
}
|
|
60
|
+
function parseScheduleBody(rest, now) {
|
|
61
|
+
const trimmed = rest.trim();
|
|
62
|
+
if (!trimmed)
|
|
63
|
+
return { error: SCHEDULE_USAGE };
|
|
64
|
+
const tokens = trimmed.split(/\s+/);
|
|
65
|
+
const head = tokens[0].toLowerCase();
|
|
66
|
+
if (head === 'at') {
|
|
67
|
+
return parseAtSchedule(tokens, now, parseTimeOfDay, fmtTime, nextOccurrenceOfTime);
|
|
68
|
+
}
|
|
69
|
+
if (head === 'on') {
|
|
70
|
+
return parseOnSchedule(tokens, now, parseMonthDay, parseTimeOfDay, fmtTime, nextDateTime);
|
|
71
|
+
}
|
|
72
|
+
if (head === 'every') {
|
|
73
|
+
return parseEverySchedule(tokens, now, parseInterval, parseTimeOfDay, fmtTime, nextOccurrenceOfTime, nextWeekday);
|
|
74
|
+
}
|
|
75
|
+
return { error: SCHEDULE_USAGE };
|
|
76
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Line types with no meaningful text to search: a terminal card renders a live PTY (no static
|
|
2
|
+
// text) and a spacer is blank.
|
|
3
|
+
const UNSEARCHABLE = new Set(['terminal', 'spacer']);
|
|
4
|
+
// Compile a search pattern as a case-insensitive regex, or null for an empty or invalid pattern.
|
|
5
|
+
// Pure — no Node imports — so both the server command and the web client (via the `@shared`
|
|
6
|
+
// alias) can use it.
|
|
7
|
+
export function compilePattern(pattern) {
|
|
8
|
+
if (!pattern)
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
return new RegExp(pattern, 'i');
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Indices (into `lines`) of every searchable line whose text matches `pattern`, oldest first.
|
|
18
|
+
// Returns [] for an empty or invalid pattern.
|
|
19
|
+
export function findMatches(lines, pattern) {
|
|
20
|
+
const regex = compilePattern(pattern);
|
|
21
|
+
if (!regex)
|
|
22
|
+
return [];
|
|
23
|
+
const indices = [];
|
|
24
|
+
for (const [index, line] of lines.entries()) {
|
|
25
|
+
if (UNSEARCHABLE.has(line.type))
|
|
26
|
+
continue;
|
|
27
|
+
if (regex.test(line.text))
|
|
28
|
+
indices.push(index);
|
|
29
|
+
}
|
|
30
|
+
return indices;
|
|
31
|
+
}
|
|
32
|
+
// The first match's [start, end) character range in `text`, for substring highlighting. Null
|
|
33
|
+
// when the pattern doesn't match (or is empty/invalid).
|
|
34
|
+
export function matchRange(text, pattern) {
|
|
35
|
+
const regex = compilePattern(pattern);
|
|
36
|
+
const match = regex?.exec(text);
|
|
37
|
+
if (!match)
|
|
38
|
+
return null;
|
|
39
|
+
return { start: match.index, end: match.index + match[0].length };
|
|
40
|
+
}
|
package/dist/security.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/** Mint a per-session token handed to the browser via the launch URL. */
|
|
3
|
+
export function makeToken() {
|
|
4
|
+
return randomBytes(24).toString('base64url');
|
|
5
|
+
}
|
|
6
|
+
// A request is only accepted when its Host (and Origin, when present) are loopback. This is the
|
|
7
|
+
// DNS-rebinding / stray-tab guard: even bound to 127.0.0.1, a remote page could otherwise try to
|
|
8
|
+
// reach the shell server with a spoofed Host header.
|
|
9
|
+
// Anchored at both ends; no nested quantifiers — not a ReDoS risk.
|
|
10
|
+
// eslint-disable-next-line security/detect-unsafe-regex
|
|
11
|
+
const LOOPBACK = /^(127\.0\.0\.1|\[::1\]|::1|localhost)(:\d+)?$/i;
|
|
12
|
+
export function originAllowed(request) {
|
|
13
|
+
const host = request.headers.host ?? '';
|
|
14
|
+
if (!LOOPBACK.test(host))
|
|
15
|
+
return false;
|
|
16
|
+
const origin = request.headers.origin;
|
|
17
|
+
if (origin) {
|
|
18
|
+
try {
|
|
19
|
+
if (!LOOPBACK.test(new URL(origin).host))
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
/** Extract the `token` query param from a request URL. */
|
|
29
|
+
export function tokenFromReq(request) {
|
|
30
|
+
try {
|
|
31
|
+
return new URL(request.url ?? '', 'http://localhost').searchParams.get('token');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Constant-time token comparison. */
|
|
38
|
+
export function tokenMatches(expected, got) {
|
|
39
|
+
if (!got)
|
|
40
|
+
return false;
|
|
41
|
+
const a = Buffer.from(expected);
|
|
42
|
+
const b = Buffer.from(got);
|
|
43
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
44
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawnShell, executeShellCmd as executeShellCommand, queryShellPwd } from './shell.js';
|
|
2
|
+
import { getConfig } from './config.js';
|
|
3
|
+
import { getGithubToken } from './github-token.js';
|
|
4
|
+
import { messageBus } from './bus.js';
|
|
5
|
+
// The base name of the user's login shell (`bash`, `zsh`, …), used both to launch tab shells and to
|
|
6
|
+
// label the `shell:<name>` connection in the panel/completion.
|
|
7
|
+
export const SHELL_NAME = (process.env.SHELL || 'bash').split('/').pop() || 'bash';
|
|
8
|
+
// Owns the per-tab persistent shells. Each tab (keyed by its label) gets one long-lived shell process
|
|
9
|
+
// that preserves working directory and environment across commands; the manager spawns them lazily,
|
|
10
|
+
// runs commands with streaming output, and tears them down.
|
|
11
|
+
export class ShellManager {
|
|
12
|
+
managers;
|
|
13
|
+
shells = new Map();
|
|
14
|
+
// Serializes each tab's shell interactions (a command's execution, then its trailing pwd query)
|
|
15
|
+
// so at most one stdin write / stdout listener pair is ever live on a given shell at a time.
|
|
16
|
+
// Without this, a rapid-fire queued command (dispatched the instant the previous one goes idle)
|
|
17
|
+
// could attach its own listener while the previous command's still-in-flight pwd query is
|
|
18
|
+
// waiting on the same stdout stream — Node delivers that chunk to both listeners, leaking the
|
|
19
|
+
// pwd query's cwd line and its `__PWD_...__` marker into the next command's output.
|
|
20
|
+
shellQueues = new Map();
|
|
21
|
+
constructor(managers) {
|
|
22
|
+
this.managers = managers;
|
|
23
|
+
}
|
|
24
|
+
// Whether a tab currently has a live shell. Drives the connections panel and completion.
|
|
25
|
+
has(label) {
|
|
26
|
+
return this.shells.has(label);
|
|
27
|
+
}
|
|
28
|
+
// The tab's persistent shell, spawned on first use and respawned if the previous one died (its
|
|
29
|
+
// stdin no longer writable). A freshly spawned shell is `cd`'d into `cwd` so it starts in the tab's
|
|
30
|
+
// working directory — the workspace clone for a workspaced agent, or the saved cwd for a
|
|
31
|
+
// `--relaunch`'d tab. A nullish `cwd` leaves the shell in its default directory.
|
|
32
|
+
getShell(label, cwd) {
|
|
33
|
+
let shell = this.shells.get(label);
|
|
34
|
+
if (!shell || !shell.stdin?.writable) {
|
|
35
|
+
const tab = this.managers.tab.tabs.find((t) => t.label === label);
|
|
36
|
+
shell = spawnShell(0, { JANUS_AGENT_NAME: label }, {
|
|
37
|
+
workspaceDir: tab?.workspaceDir,
|
|
38
|
+
offline: tab?.offline,
|
|
39
|
+
githubToken: tab?.workspaceDir ? getGithubToken() : undefined,
|
|
40
|
+
});
|
|
41
|
+
this.shells.set(label, shell);
|
|
42
|
+
this.shellQueues.delete(label);
|
|
43
|
+
if (cwd)
|
|
44
|
+
shell.stdin.write(`cd "${cwd}"\n`);
|
|
45
|
+
}
|
|
46
|
+
return shell;
|
|
47
|
+
}
|
|
48
|
+
// Run a command with transcript streaming, busy state, and persistence. This is the high-level
|
|
49
|
+
// entry point for shell execution: it creates a running transcript entry, streams output as it
|
|
50
|
+
// arrives, finalizes the entry on completion, and persists the tab. Accepts an optional callback
|
|
51
|
+
// for when the full output is captured.
|
|
52
|
+
run(label, command, options) {
|
|
53
|
+
const index = Math.max(0, this.managers.tab.findIndex(label));
|
|
54
|
+
const cwd = this.managers.tab.cwdOf(label) ?? process.cwd();
|
|
55
|
+
const tab = this.managers.tab.tabs.find((t) => t.label === label);
|
|
56
|
+
if (!tab) {
|
|
57
|
+
options?.onComplete?.('');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const before = tab.log.length;
|
|
61
|
+
const max = getConfig().transcriptMaxLines;
|
|
62
|
+
tab.log = [...tab.log, { input: command, output: '', running: true, cwd }];
|
|
63
|
+
if (tab.log.length > max)
|
|
64
|
+
tab.log = tab.log.slice(tab.log.length - max);
|
|
65
|
+
const trimmed = before + 1 - tab.log.length;
|
|
66
|
+
if (trimmed > 0)
|
|
67
|
+
messageBus.emit('transcript', { type: 'entries:trimmed', tabLabel: label, count: trimmed });
|
|
68
|
+
messageBus.emit('transcript', { type: 'entry:appended', tabLabel: label, entry: tab.log.at(-1), tab });
|
|
69
|
+
this.managers.tab.addBusy(label);
|
|
70
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
71
|
+
const update = (output, running) => {
|
|
72
|
+
const t = this.managers.tab.tabs.find((x) => x.label === label);
|
|
73
|
+
if (t) {
|
|
74
|
+
const log = [...t.log];
|
|
75
|
+
const index_ = log.findLastIndex((e) => e.input === command && e.running);
|
|
76
|
+
if (index_ !== -1)
|
|
77
|
+
log[index_] = { ...log[index_], output, running };
|
|
78
|
+
t.log = log;
|
|
79
|
+
}
|
|
80
|
+
if (!running) {
|
|
81
|
+
this.managers.tab.deleteBusy(label);
|
|
82
|
+
this.managers.tab.persist(this.managers.tab.buildAgentState(tab));
|
|
83
|
+
}
|
|
84
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
85
|
+
};
|
|
86
|
+
this.execute(label, command, index, this.managers.tab.cwdOf(label), {
|
|
87
|
+
onChunk: (buffer) => update(buffer, true),
|
|
88
|
+
onDone: (result) => {
|
|
89
|
+
update(result, false);
|
|
90
|
+
this.managers.tab.markUnread(label);
|
|
91
|
+
if (result && tab)
|
|
92
|
+
messageBus.emit('transcript', { type: 'entry:appended', tabLabel: label, entry: { input: '', output: result }, tab });
|
|
93
|
+
options?.onComplete?.(result);
|
|
94
|
+
},
|
|
95
|
+
onPwd: (pwd) => { this.managers.tab.setCwd(label, pwd); messageBus.emit('state', { type: 'dirty' }); },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// Low-level: run a command in the tab's persistent shell, spawning it if needed. `index` tags the
|
|
99
|
+
// streamed output so concurrent tabs don't cross sentinels. After the command completes the shell's
|
|
100
|
+
// pwd is queried and reported via `onPwd`. Chained onto `shellQueues` so this command's stdin
|
|
101
|
+
// write/listener pair never overlaps a still-in-flight pwd query from the previous command on
|
|
102
|
+
// the same shell (see `shellQueues`' comment).
|
|
103
|
+
execute(label, command, index, cwd, handlers) {
|
|
104
|
+
const shell = this.getShell(label, cwd);
|
|
105
|
+
const previous = this.shellQueues.get(label) ?? Promise.resolve();
|
|
106
|
+
const next = (async () => {
|
|
107
|
+
await previous;
|
|
108
|
+
await new Promise((resolve) => {
|
|
109
|
+
executeShellCommand(shell, command, index, handlers.onChunk, (result) => {
|
|
110
|
+
handlers.onDone(result);
|
|
111
|
+
queryShellPwd(shell, index, (pwd) => {
|
|
112
|
+
if (pwd)
|
|
113
|
+
handlers.onPwd(pwd);
|
|
114
|
+
resolve();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
})();
|
|
119
|
+
this.shellQueues.set(label, next);
|
|
120
|
+
}
|
|
121
|
+
// Kill and forget a tab's shell. Returns whether a shell was actually open (drives the
|
|
122
|
+
// `connection close shell` result message). On `connection close shell` and tab close.
|
|
123
|
+
close(label) {
|
|
124
|
+
const shell = this.shells.get(label);
|
|
125
|
+
if (!shell)
|
|
126
|
+
return false;
|
|
127
|
+
shell.kill();
|
|
128
|
+
this.shells.delete(label);
|
|
129
|
+
this.shellQueues.delete(label);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
// Kill every shell (app shutdown).
|
|
133
|
+
closeAll() {
|
|
134
|
+
for (const [, shell] of this.shells)
|
|
135
|
+
shell.kill();
|
|
136
|
+
this.shells.clear();
|
|
137
|
+
this.shellQueues.clear();
|
|
138
|
+
}
|
|
139
|
+
}
|
package/dist/shell.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { sandboxSpawn } from './sandbox/index.js';
|
|
3
|
+
// `sandbox`, when given a `workspaceDir`, confines the shell (and everything it spawns) to that
|
|
4
|
+
// workspace (see sandbox.ts); omitted or workspaceDir-less, the shell runs exactly as before.
|
|
5
|
+
export function spawnShell(_tabIndex, extraEnvironment, sandbox) {
|
|
6
|
+
const baseEnv = { ...process.env, ...extraEnvironment };
|
|
7
|
+
const { command, args, env } = sandboxSpawn(sandbox ?? {}, process.env.SHELL || 'bash', ['--norc', '--noprofile'], baseEnv);
|
|
8
|
+
const shell = spawn(command, args, {
|
|
9
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
10
|
+
env,
|
|
11
|
+
});
|
|
12
|
+
shell.stdout.setEncoding('utf8');
|
|
13
|
+
shell.stderr.setEncoding('utf8');
|
|
14
|
+
// Swallow EPIPE on stdin: the shell may exit while a write is in flight (e.g. during test cleanup).
|
|
15
|
+
shell.stdin.on('error', (err) => { if (err.code !== 'EPIPE')
|
|
16
|
+
throw err; });
|
|
17
|
+
return shell;
|
|
18
|
+
}
|
|
19
|
+
export function executeShellCmd(shell, command, tabIndex, onProgress, onComplete) {
|
|
20
|
+
const prompt = `__JS_END_${tabIndex}_${Date.now()}__`;
|
|
21
|
+
let outputBuffer = '';
|
|
22
|
+
const done = () => {
|
|
23
|
+
shell.stdout.removeListener('data', onChunk);
|
|
24
|
+
shell.stderr.removeListener('data', onChunk);
|
|
25
|
+
};
|
|
26
|
+
const onChunk = (chunk) => {
|
|
27
|
+
outputBuffer += chunk;
|
|
28
|
+
const endIndex = outputBuffer.indexOf(prompt);
|
|
29
|
+
if (endIndex === -1) {
|
|
30
|
+
onProgress(outputBuffer);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const result = outputBuffer.slice(0, Math.max(0, endIndex)).trim();
|
|
34
|
+
done();
|
|
35
|
+
onComplete(result);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
shell.stdout.on('data', onChunk);
|
|
39
|
+
shell.stderr.on('data', onChunk);
|
|
40
|
+
if (shell.stdin?.writable)
|
|
41
|
+
shell.stdin.write(`${command} 2>&1\necho "${prompt}"\n`);
|
|
42
|
+
}
|
|
43
|
+
export function queryShellPwd(shell, tabIndex, onResult) {
|
|
44
|
+
const prompt = `__PWD_${tabIndex}_${Date.now()}__`;
|
|
45
|
+
let buffer = '';
|
|
46
|
+
const onData = (chunk) => {
|
|
47
|
+
buffer += chunk;
|
|
48
|
+
const endIndex = buffer.indexOf(prompt);
|
|
49
|
+
if (endIndex !== -1) {
|
|
50
|
+
shell.stdout.removeListener('data', onData);
|
|
51
|
+
shell.stderr.removeListener('data', onData);
|
|
52
|
+
onResult(buffer.slice(0, Math.max(0, endIndex)).trim());
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
shell.stdout.on('data', onData);
|
|
56
|
+
shell.stderr.on('data', onData);
|
|
57
|
+
if (shell.stdin?.writable)
|
|
58
|
+
shell.stdin.write(`pwd\necho "${prompt}"\n`);
|
|
59
|
+
}
|