@detergent-software/conductor 3.0.0-dev.4 → 3.0.0-dev.41
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/CHANGELOG.md +352 -0
- package/README.md +274 -124
- package/build/brief-cli.js +5 -12
- package/build/brief-cli.js.map +1 -1
- package/build/brief.js +4 -3
- package/build/brief.js.map +1 -1
- package/build/broker-cli.js +15 -10
- package/build/broker-cli.js.map +1 -1
- package/build/broker.js +1 -1
- package/build/broker.js.map +1 -1
- package/build/claude.js +48 -2
- package/build/claude.js.map +1 -1
- package/build/cli.js +225 -71
- package/build/cli.js.map +1 -1
- package/build/clones.js +92 -0
- package/build/clones.js.map +1 -0
- package/build/code-host-cli.js +172 -40
- package/build/code-host-cli.js.map +1 -1
- package/build/code-host-service.js +35 -2
- package/build/code-host-service.js.map +1 -1
- package/build/code-host.js +140 -16
- package/build/code-host.js.map +1 -1
- package/build/conditions.js +33 -16
- package/build/conditions.js.map +1 -1
- package/build/config.js +58 -28
- package/build/config.js.map +1 -1
- package/build/console.js +43 -27
- package/build/console.js.map +1 -1
- package/build/credentials.js +86 -54
- package/build/credentials.js.map +1 -1
- package/build/daemon.js +57 -66
- package/build/daemon.js.map +1 -1
- package/build/file-lock.js +155 -40
- package/build/file-lock.js.map +1 -1
- package/build/follow-ups.js +2 -2
- package/build/follow-ups.js.map +1 -1
- package/build/help.js +237 -82
- package/build/help.js.map +1 -1
- package/build/history.js +10 -4
- package/build/history.js.map +1 -1
- package/build/host/credential-manager.js +109 -0
- package/build/host/credential-manager.js.map +1 -0
- package/build/host/keychain.js +44 -0
- package/build/host/keychain.js.map +1 -0
- package/build/host/launchd.js +118 -0
- package/build/host/launchd.js.map +1 -0
- package/build/host/libsecret.js +40 -0
- package/build/host/libsecret.js.map +1 -0
- package/build/host/macos-terminal.js +63 -0
- package/build/host/macos-terminal.js.map +1 -0
- package/build/host/platform.js +4 -0
- package/build/host/platform.js.map +1 -0
- package/build/host/secrets.js +46 -0
- package/build/host/secrets.js.map +1 -0
- package/build/host/shell.js +26 -0
- package/build/host/shell.js.map +1 -0
- package/build/host/startup.js +26 -0
- package/build/host/startup.js.map +1 -0
- package/build/host/systemd.js +91 -0
- package/build/host/systemd.js.map +1 -0
- package/build/host/task-scheduler.js +78 -0
- package/build/host/task-scheduler.js.map +1 -0
- package/build/host/terminal.js +46 -0
- package/build/host/terminal.js.map +1 -0
- package/build/host/tmux.js +39 -0
- package/build/host/tmux.js.map +1 -0
- package/build/host/windows-terminal.js +46 -0
- package/build/host/windows-terminal.js.map +1 -0
- package/build/in-process.js +6 -5
- package/build/in-process.js.map +1 -1
- package/build/inbox.js +57 -39
- package/build/inbox.js.map +1 -1
- package/build/init.js +509 -0
- package/build/init.js.map +1 -0
- package/build/landing.js +107 -10
- package/build/landing.js.map +1 -1
- package/build/lib/providers/ado/code-host.js +5 -1
- package/build/lib/providers/ado/code-host.js.map +1 -1
- package/build/lib/providers/ado/pipelines.js +138 -26
- package/build/lib/providers/ado/pipelines.js.map +1 -1
- package/build/lib/providers/ado/trigger.js +5 -0
- package/build/lib/providers/ado/trigger.js.map +1 -1
- package/build/lib/providers/ado/writes.js +17 -2
- package/build/lib/providers/ado/writes.js.map +1 -1
- package/build/lib/providers/github/actions.js +90 -3
- package/build/lib/providers/github/actions.js.map +1 -1
- package/build/lib/providers/github/code-host.js +2 -0
- package/build/lib/providers/github/code-host.js.map +1 -1
- package/build/lib/providers/github/writes.js +25 -2
- package/build/lib/providers/github/writes.js.map +1 -1
- package/build/lib/providers/index.js +4 -3
- package/build/lib/providers/index.js.map +1 -1
- package/build/lib/providers/npm/stage.js +28 -5
- package/build/lib/providers/npm/stage.js.map +1 -1
- package/build/lib/providers/timing.js +10 -0
- package/build/lib/providers/timing.js.map +1 -0
- package/build/lib/providers/verdicts/pr.js +47 -6
- package/build/lib/providers/verdicts/pr.js.map +1 -1
- package/build/lib/providers/verdicts/runs.js +12 -2
- package/build/lib/providers/verdicts/runs.js.map +1 -1
- package/build/logins.js +13 -6
- package/build/logins.js.map +1 -1
- package/build/manager.js +131 -55
- package/build/manager.js.map +1 -1
- package/build/note.js +5 -15
- package/build/note.js.map +1 -1
- package/build/notes.js +130 -23
- package/build/notes.js.map +1 -1
- package/build/notify.js +104 -42
- package/build/notify.js.map +1 -1
- package/build/npm-reads.js +323 -0
- package/build/npm-reads.js.map +1 -0
- package/build/ownership.js +2 -2
- package/build/paths.js +44 -1
- package/build/paths.js.map +1 -1
- package/build/plugin.js +4 -2
- package/build/plugin.js.map +1 -1
- package/build/profile-fields.js +153 -0
- package/build/profile-fields.js.map +1 -0
- package/build/profile-layers.js +264 -0
- package/build/profile-layers.js.map +1 -0
- package/build/profile.js +107 -51
- package/build/profile.js.map +1 -1
- package/build/project-check-cli.js +19 -19
- package/build/project-check-cli.js.map +1 -1
- package/build/project-check.js +194 -108
- package/build/project-check.js.map +1 -1
- package/build/protocol.js +5 -21
- package/build/protocol.js.map +1 -1
- package/build/pushover.js +5 -5
- package/build/pushover.js.map +1 -1
- package/build/queue.js +4 -4
- package/build/queue.js.map +1 -1
- package/build/registry.js +195 -36
- package/build/registry.js.map +1 -1
- package/build/release-kinds.js +34 -0
- package/build/release-kinds.js.map +1 -0
- package/build/rule-actions.js +8 -6
- package/build/rule-actions.js.map +1 -1
- package/build/rules-service.js +104 -47
- package/build/rules-service.js.map +1 -1
- package/build/rules.js +23 -23
- package/build/rules.js.map +1 -1
- package/build/run.js +20 -7
- package/build/run.js.map +1 -1
- package/build/runs.js +218 -68
- package/build/runs.js.map +1 -1
- package/build/scheduler.js +89 -13
- package/build/scheduler.js.map +1 -1
- package/build/search.js +2 -1
- package/build/search.js.map +1 -1
- package/build/server.js +51 -14
- package/build/server.js.map +1 -1
- package/build/spend.js +1 -1
- package/build/spend.js.map +1 -1
- package/build/store.js +79 -20
- package/build/store.js.map +1 -1
- package/build/supervise.js +1 -1
- package/build/supervise.js.map +1 -1
- package/build/timeline.js +104 -0
- package/build/timeline.js.map +1 -0
- package/build/tracker-reads-cli.js +4 -5
- package/build/tracker-reads-cli.js.map +1 -1
- package/build/trackers.js +56 -10
- package/build/trackers.js.map +1 -1
- package/build/train-cli.js +6 -5
- package/build/train-cli.js.map +1 -1
- package/build/train.js +2 -2
- package/build/train.js.map +1 -1
- package/build/upgrade.js +8 -9
- package/build/upgrade.js.map +1 -1
- package/build/wait.js +10 -10
- package/build/wait.js.map +1 -1
- package/build/watchers/ado.js +44 -20
- package/build/watchers/ado.js.map +1 -1
- package/build/watchers/github.js +21 -6
- package/build/watchers/github.js.map +1 -1
- package/build/watchers/index.js +2 -2
- package/build/watchers/index.js.map +1 -1
- package/build/watchers/linear-queue.js +47 -14
- package/build/watchers/linear-queue.js.map +1 -1
- package/build/watchers/linear-writes.js +26 -12
- package/build/watchers/linear-writes.js.map +1 -1
- package/build/watchers/linear.js +2 -2
- package/build/watchers/linear.js.map +1 -1
- package/build/watchers/npm.js +6 -4
- package/build/watchers/npm.js.map +1 -1
- package/build/watchers/source.js +5 -1
- package/build/watchers/source.js.map +1 -1
- package/build/webhook.js +80 -0
- package/build/webhook.js.map +1 -0
- package/build/why.js +1 -1
- package/build/why.js.map +1 -1
- package/build/worktrees.js +56 -93
- package/build/worktrees.js.map +1 -1
- package/package.json +61 -61
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/hooks/model.ts +46 -6
- package/plugin/hooks/register.tsx +41 -2
- package/plugin/skills/conductor-console/SKILL.md +26 -27
- package/plugin/skills/work-item/SKILL.md +58 -54
- package/plugin/skills/work-item/trackers/ado-boards.md +2 -2
- package/plugin/skills/work-item/trackers/github-issues.md +1 -1
- package/plugin/skills/work-item/trackers/linear.md +2 -2
- package/plugin/types/index.d.ts +1 -1
- package/protocol/PROTOCOL.md +3 -9
- package/build/checkout-sync.js +0 -249
- package/build/checkout-sync.js.map +0 -1
- package/build/credential.js +0 -66
- package/build/credential.js.map +0 -1
- package/build/startup.js +0 -92
- package/build/startup.js.map +0 -1
- package/build/tab.js +0 -32
- package/build/tab.js.map +0 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// macOS generic passwords in the login keychain, through `security`: the target is the item's
|
|
2
|
+
// service, its user name the account. `security` answers 44 for an item that isn't there.
|
|
3
|
+
const NOT_FOUND = 44;
|
|
4
|
+
// The item's account from `security find-generic-password`'s attributes: `"acct"<blob>="linear"`.
|
|
5
|
+
// An account saved as hex (not printable) reads as none.
|
|
6
|
+
export function account(attributes) {
|
|
7
|
+
return /"acct"<blob>="((?:[^"\\]|\\.)*)"/.exec(attributes)?.[1] ?? '';
|
|
8
|
+
}
|
|
9
|
+
export function keychain(run, keychainFile) {
|
|
10
|
+
// A keychain file of its own, for the real-OS tests; else the user's search list.
|
|
11
|
+
const where = keychainFile === undefined ? [] : [keychainFile];
|
|
12
|
+
const find = (args, env) => run('security', ['find-generic-password', ...args, ...where], { env, timeoutMs: 30_000 });
|
|
13
|
+
// Never echo stdout in a reason: on a partial failure it could hold the secret.
|
|
14
|
+
const failed = (target, result) => {
|
|
15
|
+
const why = result.error ?? (result.stderr.trim() || `exit ${result.code}`);
|
|
16
|
+
if (/ENOENT/.test(why))
|
|
17
|
+
return { found: false, missing: true, reason: 'no `security` command to ask' };
|
|
18
|
+
return { found: false, missing: false, reason: `the keychain item ${target} couldn't be read (${why})` };
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
name: 'the login keychain',
|
|
22
|
+
// The attributes first (no secret is printed without -w), then the secret of the item with that
|
|
23
|
+
// account, so the two come from one item even when another has the same service.
|
|
24
|
+
read: async (target, env) => {
|
|
25
|
+
const attributes = await find(['-s', target], env);
|
|
26
|
+
if (attributes.code === NOT_FOUND)
|
|
27
|
+
return { found: false, missing: true, reason: `no keychain item ${target}` };
|
|
28
|
+
if (attributes.code !== 0)
|
|
29
|
+
return failed(target, attributes);
|
|
30
|
+
const user = account(attributes.stdout);
|
|
31
|
+
const result = await find(['-s', target, ...(user ? ['-a', user] : []), '-w'], env);
|
|
32
|
+
const secret = result.stdout.replace(/\r?\n$/, '');
|
|
33
|
+
if (result.code !== 0 || !secret)
|
|
34
|
+
return failed(target, result);
|
|
35
|
+
return { credential: { secret, user }, found: true };
|
|
36
|
+
},
|
|
37
|
+
// Delete first: an item is one per service and account, so saving under another user name would
|
|
38
|
+
// leave the old item beside it. -w last with no value: security asks for the password.
|
|
39
|
+
saveCommand: (target, user) => `security delete-generic-password -s ${target}; security add-generic-password -s ${target} -a ${user} -w`,
|
|
40
|
+
// A missing item costs read one call, the same as asking first.
|
|
41
|
+
saved: async () => 'unknown',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=keychain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keychain.js","sourceRoot":"","sources":["../../src/host/keychain.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { ConductorError } from "../errors.js";
|
|
4
|
+
import { outputOf } from "../run.js";
|
|
5
|
+
export const LAUNCHD_LABEL = 'software.detergent.conductor';
|
|
6
|
+
// How long launchd gives supervise to stop its daemon after a SIGTERM before it kills it: a stop
|
|
7
|
+
// waits up to 20 s, and launchd's own default is 20 s.
|
|
8
|
+
const EXIT_TIMEOUT_S = 60;
|
|
9
|
+
const UNLOAD_POLL_MS = 250;
|
|
10
|
+
// A launchd agent in ~/Library/LaunchAgents that runs `conduct supervise` as the user logs in
|
|
11
|
+
// (RunAtLoad). launchd loads it into the login session's gui/<uid> domain, so loading it starts it
|
|
12
|
+
// too. supervise restarts the daemon and conduct stop must stick, so no KeepAlive; and launchd
|
|
13
|
+
// would kill the job's process group as supervise ends, workers included, without
|
|
14
|
+
// AbandonProcessGroup. Its output goes to ~/.conductor/logs/startup.log.
|
|
15
|
+
// Unloading signals supervise and returns before launchd has removed the job, so the reload waits
|
|
16
|
+
// until it has: by then supervise has stopped its daemon and the lock is free.
|
|
17
|
+
export function launchd({ home, run, uid }) {
|
|
18
|
+
const plist = join(home, 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
19
|
+
const domain = `gui/${uid}`;
|
|
20
|
+
const service = `${domain}/${LAUNCHD_LABEL}`;
|
|
21
|
+
const launchctl = (args) => run('launchctl', args, { timeoutMs: 60_000 });
|
|
22
|
+
const loaded = async () => (await launchctl(['print', service])).code === 0;
|
|
23
|
+
const bootstrap = async () => {
|
|
24
|
+
const result = await launchctl(['bootstrap', domain, plist]);
|
|
25
|
+
if (result.code !== 0)
|
|
26
|
+
throw launchctlFailed('could not load the launchd agent', result);
|
|
27
|
+
};
|
|
28
|
+
const text = () => (existsSync(plist) ? readFileSync(plist, 'utf8') : undefined);
|
|
29
|
+
return {
|
|
30
|
+
install: async (spec) => {
|
|
31
|
+
const logs = join(home, '.conductor', 'logs');
|
|
32
|
+
mkdirSync(dirname(plist), { recursive: true });
|
|
33
|
+
// launchd opens the log before supervise runs, and makes no folder for it.
|
|
34
|
+
mkdirSync(logs, { recursive: true });
|
|
35
|
+
writeFileSync(plist, launchdPlist(spec, join(logs, 'startup.log')));
|
|
36
|
+
// A loaded agent keeps the definition it was loaded with, so it goes first: that ends the
|
|
37
|
+
// daemon it started, and loading starts it again.
|
|
38
|
+
if (await loaded()) {
|
|
39
|
+
const unloaded = await launchctl(['bootout', service]);
|
|
40
|
+
const until = Date.now() + (EXIT_TIMEOUT_S + 30) * 1000;
|
|
41
|
+
while (await loaded()) {
|
|
42
|
+
if (Date.now() > until)
|
|
43
|
+
throw launchctlFailed('could not unload the launchd agent', unloaded);
|
|
44
|
+
await new Promise((done) => setTimeout(done, UNLOAD_POLL_MS));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await bootstrap();
|
|
48
|
+
return ['launchd started it as it loaded it'];
|
|
49
|
+
},
|
|
50
|
+
name: `the launchd agent ${LAUNCHD_LABEL}`,
|
|
51
|
+
// The plist alone goes: the loaded agent stays until logout, so a daemon it started keeps
|
|
52
|
+
// running, as on Windows.
|
|
53
|
+
remove: async () => {
|
|
54
|
+
if (!existsSync(plist))
|
|
55
|
+
return false;
|
|
56
|
+
rmSync(plist);
|
|
57
|
+
return true;
|
|
58
|
+
},
|
|
59
|
+
// An agent unloaded since logon (or never loaded) starts as it loads. kickstart does nothing to
|
|
60
|
+
// a supervise still ending, and the start falls back after its wait, as with the Windows task.
|
|
61
|
+
run: async () => {
|
|
62
|
+
if (!(await loaded()))
|
|
63
|
+
return bootstrap();
|
|
64
|
+
const result = await launchctl(['kickstart', service]);
|
|
65
|
+
if (result.code !== 0)
|
|
66
|
+
throw launchctlFailed('could not start the launchd agent', result);
|
|
67
|
+
},
|
|
68
|
+
runsCli: async (cliPath) => text()?.includes(`<string>${xml(cliPath)}</string>`) ?? false,
|
|
69
|
+
supervises: async () => /<string>supervise<\/string>\s*<\/array>/.test(text() ?? ''),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// The agent's plist.
|
|
73
|
+
export function launchdPlist({ cliPath, nodePath, path }, log) {
|
|
74
|
+
const string = (value) => `<string>${xml(value)}</string>`;
|
|
75
|
+
return [
|
|
76
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
77
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
78
|
+
'<plist version="1.0">',
|
|
79
|
+
'<dict>',
|
|
80
|
+
' <key>Label</key>',
|
|
81
|
+
` ${string(LAUNCHD_LABEL)}`,
|
|
82
|
+
' <key>ProgramArguments</key>',
|
|
83
|
+
' <array>',
|
|
84
|
+
...[nodePath, cliPath, 'supervise'].map((arg) => ` ${string(arg)}`),
|
|
85
|
+
' </array>',
|
|
86
|
+
' <key>EnvironmentVariables</key>',
|
|
87
|
+
' <dict>',
|
|
88
|
+
' <key>PATH</key>',
|
|
89
|
+
` ${string(path)}`,
|
|
90
|
+
' </dict>',
|
|
91
|
+
' <key>RunAtLoad</key>',
|
|
92
|
+
' <true/>',
|
|
93
|
+
' <key>AbandonProcessGroup</key>',
|
|
94
|
+
' <true/>',
|
|
95
|
+
' <key>ExitTimeOut</key>',
|
|
96
|
+
` <integer>${EXIT_TIMEOUT_S}</integer>`,
|
|
97
|
+
' <key>StandardOutPath</key>',
|
|
98
|
+
` ${string(log)}`,
|
|
99
|
+
' <key>StandardErrorPath</key>',
|
|
100
|
+
` ${string(log)}`,
|
|
101
|
+
'</dict>',
|
|
102
|
+
'</plist>',
|
|
103
|
+
'',
|
|
104
|
+
].join('\n');
|
|
105
|
+
}
|
|
106
|
+
function launchctlFailed(what, result) {
|
|
107
|
+
if (/ENOENT/.test(result.error ?? '')) {
|
|
108
|
+
return new ConductorError(`${what}: no launchctl`, ['start `conduct supervise` from your own login items']);
|
|
109
|
+
}
|
|
110
|
+
return new ConductorError(what, [
|
|
111
|
+
outputOf(result, 1000),
|
|
112
|
+
'launchd loads an agent into a logged-in session: run it in a terminal on the Mac, not over ssh alone',
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
function xml(text) {
|
|
116
|
+
return text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=launchd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launchd.js","sourceRoot":"","sources":["../../src/host/launchd.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAM1C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,MAAM,CAAC,MAAM,aAAa,GAAG,8BAA8B,CAAC;AAE5D,iGAAiG;AACjG,uDAAuD;AACvD,MAAM,cAAc,GAAG,EAAE,CAAC;AAC1B,MAAM,cAAc,GAAG,GAAG,CAAC;AAE3B,8FAA8F;AAC9F,mGAAmG;AACnG,+FAA+F;AAC/F,kFAAkF;AAClF,yEAAyE;AACzE,kGAAkG;AAClG,+EAA+E;AAC/E,MAAM,UAAU,OAAO,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAA2C;IACjF,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,aAAa,QAAQ,CAAC,CAAC;IAC9E,MAAM,MAAM,GAAG,OAAO,GAAG,EAAE,CAAC;IAC5B,MAAM,OAAO,GAAG,GAAG,MAAM,IAAI,aAAa,EAAE,CAAC;IAC7C,MAAM,SAAS,GAAG,CAAC,IAAuB,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7F,MAAM,MAAM,GAAG,KAAK,IAAsB,EAAE,CAAC,CAAC,MAAM,SAAS,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC;IAC9F,MAAM,SAAS,GAAG,KAAK,IAAmB,EAAE;QAC1C,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;QAC7D,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC;YAAE,MAAM,eAAe,CAAC,kCAAkC,EAAE,MAAM,CAAC,CAAC;IAC3F,CAAC,CAAC;IACF,MAAM,IAAI,GAAG,GAAuB,EAAE,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAErG,OAAO;QACL,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YACtB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC;YAC9C,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,2EAA2E;YAC3E,SAAS,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACrC,aAAa,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;YACpE,0FAA0F;YAC1F,kDAAkD;YAClD,IAAI,MAAM,MAAM,EAAE,EAAE,CAAC;gBACnB,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;gBACvD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,cAAc,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC;gBACxD,OAAO,MAAM,MAAM,EAAE,EAAE,CAAC;oBACtB,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;wBAAE,MAAM,eAAe,CAAC,oCAAoC,EAAE,QAAQ,CAAC,CAAC;oBAC9F,MAAM,IAAI,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC;gBAChE,CAAC;YACH,CAAC;YACD,MAAM,SAAS,EAAE,CAAC;YAClB,OAAO,CAAC,oCAAoC,CAAC,CAAC;QAChD,CAAC;QACD,IAAI,EAAE,qBAAqB,aAAa,EAAE;QAC1C,0FAA0F;QAC1F,0BAA0B;QAC1B,MAAM,EAAE,KAAK,IAAI,EAAE;YACjB,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;gBAAE,OAAO,KAAK,CAAC;YACrC,MAAM,CAAC,KAAK,CAAC,CAAC;YACd,OAAO,IAAI,CAAC;QACd,CAAC;QACD,gGAAgG;QAChG,+FAA+F;QAC/F,GAAG,EAAE,KAAK,IAAI,EAAE;YACd,IAAI,CAAC,CAAC,MAAM,MAAM,EAAE,CAAC;gBAAE,OAAO,SAAS,EAAE,CAAC;YAC1C,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;YACvD,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC;gBAAE,MAAM,eAAe,CAAC,mCAAmC,EAAE,MAAM,CAAC,CAAC;QAC5F,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,WAAW,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,KAAK;QACzF,UAAU,EAAE,KAAK,IAAI,EAAE,CAAC,yCAAyC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;KACrF,CAAC;AACJ,CAAC;AAED,qBAAqB;AACrB,MAAM,UAAU,YAAY,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAe,EAAE,GAAW;IAChF,MAAM,MAAM,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,WAAW,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC;IAC3E,OAAO;QACL,wCAAwC;QACxC,wGAAwG;QACxG,uBAAuB;QACvB,QAAQ;QACR,oBAAoB;QACpB,KAAK,MAAM,CAAC,aAAa,CAAC,EAAE;QAC5B,+BAA+B;QAC/B,WAAW;QACX,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QACtE,YAAY;QACZ,mCAAmC;QACnC,UAAU;QACV,qBAAqB;QACrB,OAAO,MAAM,CAAC,IAAI,CAAC,EAAE;QACrB,WAAW;QACX,wBAAwB;QACxB,WAAW;QACX,kCAAkC;QAClC,WAAW;QACX,0BAA0B;QAC1B,cAAc,cAAc,YAAY;QACxC,8BAA8B;QAC9B,KAAK,MAAM,CAAC,GAAG,CAAC,EAAE;QAClB,gCAAgC;QAChC,KAAK,MAAM,CAAC,GAAG,CAAC,EAAE;QAClB,SAAS;QACT,UAAU;QACV,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,SAAS,eAAe,CAAC,IAAY,EAAE,MAAiB;IACtD,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,cAAc,CAAC,GAAG,IAAI,gBAAgB,EAAE,CAAC,qDAAqD,CAAC,CAAC,CAAC;IAC9G,CAAC;IACD,OAAO,IAAI,cAAc,CAAC,IAAI,EAAE;QAC9B,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;QACtB,sGAAsG;KACvG,CAAC,CAAC;AACL,CAAC;AAED,SAAS,GAAG,CAAC,IAAY;IACvB,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AACvF,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Linux secrets in the Secret Service (GNOME Keyring, KeePassXC and others), through libsecret's
|
|
2
|
+
// `secret-tool`: an item with the attributes service=conductor and target=<target>, its user name in
|
|
3
|
+
// a `user` attribute. secret-tool needs a D-Bus session with a Secret Service on it; a headless box
|
|
4
|
+
// without one can't read, and the env fallback (secrets.ts) stands in.
|
|
5
|
+
const SERVICE = 'conductor';
|
|
6
|
+
export function libsecret(run) {
|
|
7
|
+
const tool = (args, env) => run('secret-tool', args, { env, timeoutMs: 30_000 });
|
|
8
|
+
const lookup = (target, env) => tool(['lookup', 'service', SERVICE, 'target', target], env);
|
|
9
|
+
return {
|
|
10
|
+
name: 'libsecret',
|
|
11
|
+
read: async (target, env) => {
|
|
12
|
+
const result = await lookup(target, env);
|
|
13
|
+
const secret = result.stdout.replace(/\r?\n$/, '');
|
|
14
|
+
if (result.code === 0 && secret) {
|
|
15
|
+
// search prints the item's attributes, and its secret, which only the user line is read from.
|
|
16
|
+
const found = await tool(['search', 'service', SERVICE, 'target', target], env);
|
|
17
|
+
return { credential: { secret, user: userOf(`${found.stdout}\n${found.stderr}`) }, found: true };
|
|
18
|
+
}
|
|
19
|
+
if (result.error !== null && /ENOENT/.test(result.error))
|
|
20
|
+
return { found: false, missing: true, reason: 'no secret-tool (libsecret-tools) to ask' };
|
|
21
|
+
// lookup exits 1 and says nothing for an item that isn't there.
|
|
22
|
+
if (result.code === 1 && !result.stderr.trim())
|
|
23
|
+
return { found: false, missing: true, reason: `no libsecret item ${target}` };
|
|
24
|
+
// Never echo stdout here: on a partial failure it could hold the secret.
|
|
25
|
+
const why = result.error ?? (result.stderr.trim().split(/\r?\n/)[0] || `exit ${result.code}`);
|
|
26
|
+
return { found: false, missing: false, reason: `the libsecret item ${target} couldn't be read (${why})` };
|
|
27
|
+
},
|
|
28
|
+
// Clear first: store replaces only an item whose attributes are all the same, so saving under
|
|
29
|
+
// another user name would leave the old item beside it, and lookup and search could each pick
|
|
30
|
+
// another one. secret-tool store asks for the secret.
|
|
31
|
+
saveCommand: (target, user) => `secret-tool clear service ${SERVICE} target ${target}; secret-tool store --label=${target} service ${SERVICE} target ${target} user ${user}`,
|
|
32
|
+
// Asking would be a lookup, which reads the secret: read decides instead.
|
|
33
|
+
saved: async () => 'unknown',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// The `user` attribute from `secret-tool search`: `attribute.user = linear`.
|
|
37
|
+
export function userOf(search) {
|
|
38
|
+
return /^attribute\.user = (.*)$/m.exec(search)?.[1]?.trim() ?? '';
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=libsecret.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"libsecret.js","sourceRoot":"","sources":["../../src/host/libsecret.ts"],"names":[],"mappings":"AAEA,iGAAiG;AACjG,qGAAqG;AACrG,oGAAoG;AACpG,uEAAuE;AACvE,MAAM,OAAO,GAAG,WAAW,CAAC;AAE5B,MAAM,UAAU,SAAS,CAAC,GAAQ;IAChC,MAAM,IAAI,GAAG,CAAC,IAAuB,EAAE,GAAsB,EAAE,EAAE,CAC/D,GAAG,CAAC,aAAa,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,CAAC,MAAc,EAAE,GAAsB,EAAE,EAAE,CACxD,IAAI,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC;IAE9D,OAAO;QACL,IAAI,EAAE,WAAW;QACjB,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAA2B,EAAE;YACnD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YACzC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;YACnD,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;gBAChC,8FAA8F;gBAC9F,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC;gBAChF,OAAO,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;YACnG,CAAC;YACD,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;gBACtD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,yCAAyC,EAAE,CAAC;YAC5F,gEAAgE;YAChE,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE;gBAC5C,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,qBAAqB,MAAM,EAAE,EAAE,CAAC;YAChF,yEAAyE;YACzE,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;YAC9F,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,sBAAsB,MAAM,sBAAsB,GAAG,GAAG,EAAE,CAAC;QAC5G,CAAC;QACD,8FAA8F;QAC9F,8FAA8F;QAC9F,sDAAsD;QACtD,WAAW,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAC5B,6BAA6B,OAAO,WAAW,MAAM,+BAA+B,MAAM,YAAY,OAAO,WAAW,MAAM,SAAS,IAAI,EAAE;QAC/I,0EAA0E;QAC1E,KAAK,EAAE,KAAK,IAA8B,EAAE,CAAC,SAAS;KACvD,CAAC;AACJ,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,MAAM,CAAC,MAAc;IACnC,OAAO,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACrE,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { outputOf } from "../run.js";
|
|
2
|
+
import { shLine, shQuote, viaSh } from "./shell.js";
|
|
3
|
+
// A string as AppleScript reads it, in double quotes.
|
|
4
|
+
export function appleString(text) {
|
|
5
|
+
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
6
|
+
}
|
|
7
|
+
// Terminal.app: a new window per tab (Terminal has no named windows to add a tab to), titled.
|
|
8
|
+
export function appleTerminal(run) {
|
|
9
|
+
return macTerminal(run, 'Terminal', (tab) => [
|
|
10
|
+
'tell application "Terminal"',
|
|
11
|
+
'activate',
|
|
12
|
+
`set t to do script ${appleString(macLine(tab))}`,
|
|
13
|
+
`set custom title of t to ${appleString(cleanTitle(tab.title))}`,
|
|
14
|
+
'end tell',
|
|
15
|
+
]);
|
|
16
|
+
}
|
|
17
|
+
// iTerm: a tab in its front window, or a new window when none is open, named after the tab. An
|
|
18
|
+
// iTerm that wasn't running opens a window of its own as it starts: the tab takes that window's
|
|
19
|
+
// session instead of adding one beside it.
|
|
20
|
+
export function iterm(run) {
|
|
21
|
+
return macTerminal(run, 'iTerm', (tab) => [
|
|
22
|
+
'set wasRunning to application "iTerm" is running',
|
|
23
|
+
'tell application "iTerm"',
|
|
24
|
+
'activate',
|
|
25
|
+
'if (count of windows) is 0 then',
|
|
26
|
+
'set w to (create window with default profile)',
|
|
27
|
+
'else',
|
|
28
|
+
'set w to current window',
|
|
29
|
+
'if wasRunning then tell w to create tab with default profile',
|
|
30
|
+
'end if',
|
|
31
|
+
'tell current session of w',
|
|
32
|
+
`set name to ${appleString(cleanTitle(tab.title))}`,
|
|
33
|
+
`write text ${appleString(macLine(tab))}`,
|
|
34
|
+
'end tell',
|
|
35
|
+
'end tell',
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
// What a macOS tab types into its shell: the folder and the command through sh, then `exit`, so the
|
|
39
|
+
// shell ends with the command as a Windows Terminal or tmux tab does (Terminal closes the window
|
|
40
|
+
// then if its profile says to). The shell is the user's login shell, in its own environment, so
|
|
41
|
+
// `env` goes on the line.
|
|
42
|
+
export function macLine(tab) {
|
|
43
|
+
return `${viaSh(`cd ${shQuote(tab.cwd)} && ${shLine(tab)}`)}; exit`;
|
|
44
|
+
}
|
|
45
|
+
function cleanTitle(title) {
|
|
46
|
+
return title.replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
47
|
+
}
|
|
48
|
+
// Runs the AppleScript, one `-e` per line: osascript takes it as arguments, never through a shell.
|
|
49
|
+
function macTerminal(run, name, script) {
|
|
50
|
+
return {
|
|
51
|
+
attachHint: () => null,
|
|
52
|
+
name,
|
|
53
|
+
open: async (tab) => {
|
|
54
|
+
const result = await run('osascript', script(tab).flatMap((line) => ['-e', line]), { timeoutMs: 60_000 });
|
|
55
|
+
if (result.code === 0)
|
|
56
|
+
return null;
|
|
57
|
+
return result.error !== null && /ENOENT/.test(result.error)
|
|
58
|
+
? 'no osascript to open it with'
|
|
59
|
+
: outputOf(result, 500);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=macos-terminal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"macos-terminal.js","sourceRoot":"","sources":["../../src/host/macos-terminal.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEpD,sDAAsD;AACtD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC;AACjE,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,aAAa,CAAC,GAAQ;IACpC,OAAO,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC;QAC3C,6BAA6B;QAC7B,UAAU;QACV,sBAAsB,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE;QACjD,4BAA4B,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE;QAChE,UAAU;KACX,CAAC,CAAC;AACL,CAAC;AAED,+FAA+F;AAC/F,gGAAgG;AAChG,2CAA2C;AAC3C,MAAM,UAAU,KAAK,CAAC,GAAQ;IAC5B,OAAO,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC;QACxC,kDAAkD;QAClD,0BAA0B;QAC1B,UAAU;QACV,iCAAiC;QACjC,+CAA+C;QAC/C,MAAM;QACN,yBAAyB;QACzB,8DAA8D;QAC9D,QAAQ;QACR,2BAA2B;QAC3B,eAAe,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE;QACnD,cAAc,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE;QACzC,UAAU;QACV,UAAU;KACX,CAAC,CAAC;AACL,CAAC;AAED,oGAAoG;AACpG,iGAAiG;AACjG,gGAAgG;AAChG,0BAA0B;AAC1B,MAAM,UAAU,OAAO,CAAC,GAAyD;IAC/E,OAAO,GAAG,KAAK,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC;AACtE,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACxD,CAAC;AAED,mGAAmG;AACnG,SAAS,WAAW,CAAC,GAAQ,EAAE,IAAY,EAAE,MAAqC;IAChF,OAAO;QACL,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI;QACtB,IAAI;QACJ,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAClB,MAAM,MAAM,GAAG,MAAM,GAAG,CACtB,WAAW,EACX,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAC3C,EAAE,SAAS,EAAE,MAAM,EAAE,CACtB,CAAC;YACF,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACnC,OAAO,MAAM,CAAC,KAAK,KAAK,IAAI,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;gBACzD,CAAC,CAAC,8BAA8B;gBAChC,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC5B,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/host/platform.ts"],"names":[],"mappings":"AAIA,MAAM,UAAU,YAAY,CAAC,WAA4B,OAAO,CAAC,QAAQ;IACvE,OAAO,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;AAC5E,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { run as runCommand } from "../run.js";
|
|
2
|
+
import { credentialManager } from "./credential-manager.js";
|
|
3
|
+
import { keychain } from "./keychain.js";
|
|
4
|
+
import { libsecret } from "./libsecret.js";
|
|
5
|
+
import { hostPlatform } from "./platform.js";
|
|
6
|
+
// The env var that stands in for the credential `target` where the store has none, or can't be
|
|
7
|
+
// read (a headless Linux box with no keyring): CONDUCTOR_SECRET_ and the target in upper snake
|
|
8
|
+
// case, with its user name, when one is needed, in the same name plus _USER.
|
|
9
|
+
export function secretEnvName(target) {
|
|
10
|
+
return `CONDUCTOR_SECRET_${target
|
|
11
|
+
.toUpperCase()
|
|
12
|
+
.replace(/[^A-Z0-9]+/g, '_')
|
|
13
|
+
.replace(/^_+|_+$/g, '')}`;
|
|
14
|
+
}
|
|
15
|
+
// This platform's store, with the env fallback.
|
|
16
|
+
export function secretStore({ platform = hostPlatform(), run = runCommand, } = {}) {
|
|
17
|
+
const stores = {
|
|
18
|
+
darwin: () => keychain(run),
|
|
19
|
+
linux: () => libsecret(run),
|
|
20
|
+
win32: () => credentialManager(run),
|
|
21
|
+
};
|
|
22
|
+
return withEnvFallback(stores[platform]());
|
|
23
|
+
}
|
|
24
|
+
// The store, else secretEnvName(target) from the environment.
|
|
25
|
+
export function withEnvFallback(store) {
|
|
26
|
+
const fromEnv = (target, env) => {
|
|
27
|
+
const name = secretEnvName(target);
|
|
28
|
+
const secret = env[name]?.trim();
|
|
29
|
+
return secret ? { secret, user: env[`${name}_USER`]?.trim() ?? '' } : null;
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
name: store.name,
|
|
33
|
+
read: async (target, env) => {
|
|
34
|
+
const got = await store.read(target, env);
|
|
35
|
+
if (got.found)
|
|
36
|
+
return got;
|
|
37
|
+
const credential = fromEnv(target, env);
|
|
38
|
+
return credential
|
|
39
|
+
? { credential, found: true }
|
|
40
|
+
: { ...got, reason: `${got.reason}, and no ${secretEnvName(target)}` };
|
|
41
|
+
},
|
|
42
|
+
saveCommand: (target, user) => store.saveCommand(target, user),
|
|
43
|
+
saved: async (target, env) => (fromEnv(target, env) ? 'saved' : store.saved(target, env)),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/host/secrets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,IAAI,UAAU,EAAmC,MAAM,WAAW,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAqB,YAAY,EAAE,MAAM,eAAe,CAAC;AA8BhE,+FAA+F;AAC/F,+FAA+F;AAC/F,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,OAAO,oBAAoB,MAAM;SAC9B,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,EAAE,CAAC;AAC/B,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,WAAW,CAAC,EAC1B,QAAQ,GAAG,YAAY,EAAE,EACzB,GAAG,GAAG,UAAU,MAC0B,EAAE;IAC5C,MAAM,MAAM,GAA4C;QACtD,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC;QAC3B,KAAK,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC;QAC3B,KAAK,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC;KACpC,CAAC;IACF,OAAO,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,eAAe,CAAC,KAAkB;IAChD,MAAM,OAAO,GAAG,CAAC,MAAc,EAAE,GAAsB,EAAqB,EAAE;QAC5E,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QACnC,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC;QACjC,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7E,CAAC,CAAC;IACF,OAAO;QACL,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE;YAC1B,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YAC1C,IAAI,GAAG,CAAC,KAAK;gBAAE,OAAO,GAAG,CAAC;YAC1B,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YACxC,OAAO,UAAU;gBACf,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE;gBAC7B,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,YAAY,aAAa,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QAC3E,CAAC;QACD,WAAW,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC;QAC9D,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;KAC1F,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { win32 } from 'node:path';
|
|
2
|
+
// What a held tab says when its command fails, such as "claude attach failed, press Enter to close
|
|
3
|
+
// this tab". win32's basename splits on both / and \, so a Windows path reads the same on any host.
|
|
4
|
+
export function failedText(command) {
|
|
5
|
+
const [program = '', verb] = command;
|
|
6
|
+
const name = win32.basename(program).replace(/\.(exe|cmd)$/i, '');
|
|
7
|
+
return `${verb === undefined ? name : `${name} ${verb}`} failed, press Enter to close this tab`;
|
|
8
|
+
}
|
|
9
|
+
// The tab's command as one sh line: its env first, and with `hold`, a failure that waits for Enter
|
|
10
|
+
// before the tab closes.
|
|
11
|
+
export function shLine(tab) {
|
|
12
|
+
const env = Object.entries(tab.env).map(([name, value]) => `${name}=${shQuote(value)}`);
|
|
13
|
+
const line = [...(env.length > 0 ? ['env', ...env] : []), ...tab.command.map(shQuote)].join(' ');
|
|
14
|
+
return tab.hold ? `${line} || { echo ${shQuote(failedText(tab.command))}; read _; }` : line;
|
|
15
|
+
}
|
|
16
|
+
// A word as sh reads it: bare when it can't be taken for anything else, else in single quotes.
|
|
17
|
+
export function shQuote(word) {
|
|
18
|
+
return /^[\w%+,./:=@-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
|
|
19
|
+
}
|
|
20
|
+
// A line run through sh, as one word for the shell that runs it. tmux's default-shell and a macOS
|
|
21
|
+
// login shell are the user's, which may not be sh (fish has no `{ …; }`); `sh -c '…'` reads the same
|
|
22
|
+
// in each.
|
|
23
|
+
export function viaSh(line) {
|
|
24
|
+
return `sh -c ${shQuote(line)}`;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/host/shell.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAIlC,mGAAmG;AACnG,oGAAoG;AACpG,MAAM,UAAU,UAAU,CAAC,OAA0B;IACnD,MAAM,CAAC,OAAO,GAAG,EAAE,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC;IACrC,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IAClE,OAAO,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,wCAAwC,CAAC;AAClG,CAAC;AAED,mGAAmG;AACnG,yBAAyB;AACzB,MAAM,UAAU,MAAM,CAAC,GAAiD;IACtE,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACxF,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACjG,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,cAAc,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9F,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,OAAO,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;AACnF,CAAC;AAED,kGAAkG;AAClG,qGAAqG;AACrG,WAAW;AACX,MAAM,UAAU,KAAK,CAAC,IAAY;IAChC,OAAO,SAAS,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { homedir, userInfo } from 'node:os';
|
|
2
|
+
import { run as runCommand } from "../run.js";
|
|
3
|
+
import { launchd } from "./launchd.js";
|
|
4
|
+
import { hostPlatform } from "./platform.js";
|
|
5
|
+
import {} from "./secrets.js";
|
|
6
|
+
import { systemd } from "./systemd.js";
|
|
7
|
+
import { taskScheduler } from "./task-scheduler.js";
|
|
8
|
+
// This platform's startup.
|
|
9
|
+
export function startup({ env = process.env, home = homedir(), platform = hostPlatform(), run = runCommand, uid = process.getuid?.() ?? 0, user = userName(), } = {}) {
|
|
10
|
+
const startups = {
|
|
11
|
+
darwin: () => launchd({ home, run, uid }),
|
|
12
|
+
linux: () => systemd({ env, home, run, user }),
|
|
13
|
+
win32: () => taskScheduler(run),
|
|
14
|
+
};
|
|
15
|
+
return startups[platform]();
|
|
16
|
+
}
|
|
17
|
+
// userInfo throws for a uid with no passwd entry, such as a container's.
|
|
18
|
+
function userName() {
|
|
19
|
+
try {
|
|
20
|
+
return userInfo().username;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return process.env.USER ?? '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=startup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"startup.js","sourceRoot":"","sources":["../../src/host/startup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,EAAE,GAAG,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAqB,YAAY,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAY,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAuCpD,2BAA2B;AAC3B,MAAM,UAAU,OAAO,CAAC,EACtB,GAAG,GAAG,OAAO,CAAC,GAAG,EACjB,IAAI,GAAG,OAAO,EAAE,EAChB,QAAQ,GAAG,YAAY,EAAE,EACzB,GAAG,GAAG,UAAU,EAChB,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,EAC7B,IAAI,GAAG,QAAQ,EAAE,MACF,EAAE;IACjB,MAAM,QAAQ,GAAwC;QACpD,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;QACzC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;QAC9C,KAAK,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC;KAChC,CAAC;IACF,OAAO,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;AAC9B,CAAC;AAED,yEAAyE;AACzE,SAAS,QAAQ;IACf,IAAI,CAAC;QACH,OAAO,QAAQ,EAAE,CAAC,QAAQ,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IAChC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { ConductorError } from "../errors.js";
|
|
4
|
+
import { outputOf } from "../run.js";
|
|
5
|
+
export const SYSTEMD_UNIT = 'conductor.service';
|
|
6
|
+
// A systemd user unit that runs `conduct supervise` when the user's service manager starts: at
|
|
7
|
+
// login, or at boot with lingering on. supervise restarts the daemon and conduct stop must stick,
|
|
8
|
+
// so no Restart=; and KillMode=process, or the unit ending (a stop, an upgrade) would kill its whole
|
|
9
|
+
// cgroup, the workers and tmux the daemon started included. Its output goes to the journal
|
|
10
|
+
// (journalctl --user -u conductor).
|
|
11
|
+
export function systemd({ env, home, run, user, }) {
|
|
12
|
+
// systemd reads user units from $XDG_CONFIG_HOME/systemd/user, ~/.config without it.
|
|
13
|
+
const unit = join(env.XDG_CONFIG_HOME || join(home, '.config'), 'systemd', 'user', SYSTEMD_UNIT);
|
|
14
|
+
const systemctl = async (what, args) => {
|
|
15
|
+
const result = await run('systemctl', ['--user', ...args], { timeoutMs: 60_000 });
|
|
16
|
+
if (result.code !== 0)
|
|
17
|
+
throw systemctlFailed(what, result);
|
|
18
|
+
};
|
|
19
|
+
const text = () => (existsSync(unit) ? readFileSync(unit, 'utf8') : undefined);
|
|
20
|
+
return {
|
|
21
|
+
install: async (spec) => {
|
|
22
|
+
mkdirSync(dirname(unit), { recursive: true });
|
|
23
|
+
writeFileSync(unit, systemdUnit(spec));
|
|
24
|
+
await systemctl('could not load the systemd unit', ['daemon-reload']);
|
|
25
|
+
await systemctl('could not enable the systemd unit', ['enable', SYSTEMD_UNIT]);
|
|
26
|
+
// A running one keeps the command it started with until it restarts.
|
|
27
|
+
await systemctl('could not start the systemd unit', ['restart', SYSTEMD_UNIT]);
|
|
28
|
+
const linger = await run('loginctl', ['show-user', user, '--property=Linger', '--value'], { timeoutMs: 30_000 });
|
|
29
|
+
return [
|
|
30
|
+
'systemd started it now',
|
|
31
|
+
...(linger.stdout.trim() === 'yes'
|
|
32
|
+
? []
|
|
33
|
+
: [
|
|
34
|
+
`it starts at your login; to start it at boot on a machine nobody logs in to, run \`loginctl enable-linger ${user || '$USER'}\``,
|
|
35
|
+
]),
|
|
36
|
+
];
|
|
37
|
+
},
|
|
38
|
+
name: `the systemd user unit ${SYSTEMD_UNIT}`,
|
|
39
|
+
// Disabled and gone, but not stopped: a daemon it started keeps running, as on Windows.
|
|
40
|
+
remove: async () => {
|
|
41
|
+
if (!existsSync(unit))
|
|
42
|
+
return false;
|
|
43
|
+
await systemctl('could not disable the systemd unit', ['disable', SYSTEMD_UNIT]);
|
|
44
|
+
rmSync(unit);
|
|
45
|
+
await run('systemctl', ['--user', 'daemon-reload'], { timeoutMs: 60_000 });
|
|
46
|
+
return true;
|
|
47
|
+
},
|
|
48
|
+
run: () => systemctl('could not start the systemd unit', ['start', SYSTEMD_UNIT]),
|
|
49
|
+
runsCli: async (cliPath) => execStart(text())?.includes(` ${unitWord(cliPath)} `) ?? false,
|
|
50
|
+
supervises: async () => / supervise$/.test(execStart(text()) ?? ''),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// The unit file.
|
|
54
|
+
export function systemdUnit({ cliPath, nodePath, path }) {
|
|
55
|
+
return [
|
|
56
|
+
'[Unit]',
|
|
57
|
+
'Description=The conductor daemon (conduct supervise)',
|
|
58
|
+
'',
|
|
59
|
+
'[Service]',
|
|
60
|
+
`ExecStart=${[nodePath, cliPath].map((word) => unitWord(word)).join(' ')} supervise`,
|
|
61
|
+
`Environment=${unitWord(`PATH=${path}`, false)}`,
|
|
62
|
+
'KillMode=process',
|
|
63
|
+
'',
|
|
64
|
+
'[Install]',
|
|
65
|
+
'WantedBy=default.target',
|
|
66
|
+
'',
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
function execStart(unit) {
|
|
70
|
+
return /^ExecStart=(.*)$/m.exec(unit ?? '')?.[1];
|
|
71
|
+
}
|
|
72
|
+
function systemctlFailed(what, result) {
|
|
73
|
+
if (/ENOENT/.test(result.error ?? '')) {
|
|
74
|
+
return new ConductorError(`${what}: no systemctl`, ['start `conduct supervise` from your own login items']);
|
|
75
|
+
}
|
|
76
|
+
return new ConductorError(what, [
|
|
77
|
+
outputOf(result, 1000),
|
|
78
|
+
"systemctl --user needs your login session's service manager: run it in a session you logged in to, not after su",
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
// A word in quotes, as ExecStart= and Environment= read it: `%` starts a specifier, and in
|
|
82
|
+
// ExecStart= `$` an environment variable.
|
|
83
|
+
function unitWord(text, command = true) {
|
|
84
|
+
const escaped = text
|
|
85
|
+
.replaceAll('\\', '\\\\')
|
|
86
|
+
.replaceAll('"', '\\"')
|
|
87
|
+
.replaceAll('%', '%%')
|
|
88
|
+
.replaceAll('$', () => (command ? '$$' : '$'));
|
|
89
|
+
return `"${escaped}"`;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=systemd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemd.js","sourceRoot":"","sources":["../../src/host/systemd.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ConductorError } from "../errors.js";
|
|
2
|
+
import { outputOf } from "../run.js";
|
|
3
|
+
export const TASK_NAME = 'Conductor';
|
|
4
|
+
// The task's <Arguments> quote the cli.js path (install); XML escapes it, and Windows paths compare
|
|
5
|
+
// without case.
|
|
6
|
+
export function taskRunsCli(xml, cliPath) {
|
|
7
|
+
const args = taskArguments(xml);
|
|
8
|
+
return args !== undefined && args.toLowerCase().includes(`"${cliPath.toLowerCase()}"`);
|
|
9
|
+
}
|
|
10
|
+
// A per-user Task Scheduler task that runs `conduct supervise` at logon, which keeps `conduct serve`
|
|
11
|
+
// running (supervise.ts). No elevation is needed for a task that runs as the current user.
|
|
12
|
+
// conhost --headless keeps a console window (or a Windows Terminal tab) from opening, and the zero
|
|
13
|
+
// time limit stops Task Scheduler from ending the daemon after its default 72 hours. A second start
|
|
14
|
+
// while one runs exits on the pid lock, and its supervisor with it. Installing doesn't start it.
|
|
15
|
+
export function taskScheduler(run) {
|
|
16
|
+
const schtasks = (args) => run('schtasks', args, { timeoutMs: 60_000 });
|
|
17
|
+
const taskXml = async () => {
|
|
18
|
+
const result = await schtasks(['/Query', '/TN', TASK_NAME, '/XML']);
|
|
19
|
+
return result.code === 0 ? result.stdout : undefined;
|
|
20
|
+
};
|
|
21
|
+
const installed = async () => (await schtasks(['/Query', '/TN', TASK_NAME])).code === 0;
|
|
22
|
+
return {
|
|
23
|
+
install: async ({ cliPath, nodePath }) => {
|
|
24
|
+
const script = [
|
|
25
|
+
"$ErrorActionPreference = 'Stop'",
|
|
26
|
+
'$user = "$env:USERDOMAIN\\$env:USERNAME"',
|
|
27
|
+
'$action = New-ScheduledTaskAction -Execute "$env:SystemRoot\\System32\\conhost.exe" -Argument $env:CONDUCTOR_TASK_ARGS',
|
|
28
|
+
'$trigger = New-ScheduledTaskTrigger -AtLogOn -User $user',
|
|
29
|
+
'$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit ([TimeSpan]::Zero)',
|
|
30
|
+
'$principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited',
|
|
31
|
+
`Register-ScheduledTask -TaskName '${TASK_NAME}' -Description 'Starts the conductor daemon (conduct supervise) at logon.' -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Force | Out-Null`,
|
|
32
|
+
].join('\n');
|
|
33
|
+
const result = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
34
|
+
env: { ...process.env, CONDUCTOR_TASK_ARGS: `--headless "${nodePath}" "${cliPath}" supervise` },
|
|
35
|
+
timeoutMs: 60_000,
|
|
36
|
+
});
|
|
37
|
+
if (result.code !== 0)
|
|
38
|
+
throw new ConductorError('could not register the logon task', [outputOf(result, 1000)]);
|
|
39
|
+
return [];
|
|
40
|
+
},
|
|
41
|
+
name: `the "${TASK_NAME}" logon task`,
|
|
42
|
+
remove: async () => {
|
|
43
|
+
if (!(await installed()))
|
|
44
|
+
return false;
|
|
45
|
+
const result = await schtasks(['/Delete', '/TN', TASK_NAME, '/F']);
|
|
46
|
+
if (result.code !== 0)
|
|
47
|
+
throw new ConductorError('could not remove the logon task', [outputOf(result, 1000)]);
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
run: async () => {
|
|
51
|
+
const result = await schtasks(['/Run', '/TN', TASK_NAME]);
|
|
52
|
+
if (result.code !== 0)
|
|
53
|
+
throw new ConductorError(`could not run the "${TASK_NAME}" logon task`, [outputOf(result, 1000)]);
|
|
54
|
+
},
|
|
55
|
+
runsCli: async (cliPath) => {
|
|
56
|
+
const xml = await taskXml();
|
|
57
|
+
return xml !== undefined && taskRunsCli(xml, cliPath);
|
|
58
|
+
},
|
|
59
|
+
supervises: async () => {
|
|
60
|
+
const xml = await taskXml();
|
|
61
|
+
return xml !== undefined && taskSupervises(xml);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function taskSupervises(xml) {
|
|
66
|
+
return /\ssupervise\s*$/.test(taskArguments(xml) ?? '');
|
|
67
|
+
}
|
|
68
|
+
// The task action's <Arguments>, XML unescaped.
|
|
69
|
+
function taskArguments(xml) {
|
|
70
|
+
return /<Arguments>([^<]*)<\/Arguments>/
|
|
71
|
+
.exec(xml)?.[1]
|
|
72
|
+
?.replaceAll('"', '"')
|
|
73
|
+
.replaceAll(''', "'")
|
|
74
|
+
.replaceAll('<', '<')
|
|
75
|
+
.replaceAll('>', '>')
|
|
76
|
+
.replaceAll('&', '&');
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=task-scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-scheduler.js","sourceRoot":"","sources":["../../src/host/task-scheduler.ts"],"names":[],"mappings":"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"}
|