@crvouga/dev-fleet 0.0.0-stage → 0.1.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/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
- package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
- package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
- package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
- package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
- package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/agents/antigravity-proxy.ts +3 -0
- package/agents/audit.md +89 -0
- package/agents/claude-proxy +5 -0
- package/agents/claude-proxy.ts +3 -0
- package/agents/codex-proxy.ts +3 -0
- package/agents/grok-proxy.ts +3 -0
- package/agents/managed-proxy.ts +55 -0
- package/agents/opencode-proxy.ts +3 -0
- package/agents/registry.json +209 -0
- package/agents/registry.schema.json +149 -0
- package/agents/setup.md +128 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +17 -0
- package/ansible/monitoring/README.md +154 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +134 -0
- package/ansible/monitoring/beszel-agent.service +42 -0
- package/ansible/monitoring/compose.yml +74 -0
- package/ansible/monitoring/hub-darwin.yml +102 -0
- package/ansible/monitoring/lock.json +11 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +43 -0
- package/ansible/ubuntu/recovery.yml +15 -0
- package/ansible/ubuntu/t3/policy.json +7 -0
- package/ansible/ubuntu/t3-desktop.yml +220 -0
- package/ansible/ubuntu/t3-health.sh +33 -0
- package/ansible/ubuntu/t3-policy.yml +76 -0
- package/ansible/ubuntu/t3-route.yml +28 -0
- package/ansible/ubuntu/tailscale.yml +75 -0
- package/ansible/ubuntu/tasks/credentials.yml +47 -0
- package/ansible/ubuntu/tasks/docker.yml +31 -0
- package/ansible/ubuntu/tasks/packages.yml +56 -0
- package/ansible/ubuntu/tasks/power.yml +51 -0
- package/ansible/ubuntu/tasks/recovery.yml +39 -0
- package/ansible/ubuntu/tasks/resources.yml +252 -0
- package/ansible/ubuntu/tasks/services.yml +31 -0
- package/ansible/ubuntu/tasks/ssh.yml +71 -0
- package/ansible/ubuntu/tasks/user.yml +120 -0
- package/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +73 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +189 -0
- package/bootstrap/fleet-manager-run.sh +76 -0
- package/bootstrap/fleet-remote-bootstrap.sh +185 -0
- package/bootstrap/global-config.ts +113 -0
- package/bootstrap/install-launcher.sh +7 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +350 -0
- package/bootstrap/setup-status.ts +51 -0
- package/bootstrap/setup.ts +88 -0
- package/bootstrap/shell-path.ts +105 -0
- package/bootstrap/verify-t3-archive.py +49 -0
- package/bootstrap/worker-auth.ts +65 -0
- package/bootstrap/worker-github.ts +47 -0
- package/bootstrap/worker-tools.ts +24 -0
- package/cliproxy/AGENTS.md +11 -0
- package/cliproxy/README.md +269 -0
- package/cliproxy/clients/bootstrap.py +143 -0
- package/cliproxy/clients/launcher.py +57 -0
- package/cliproxy/compose.yaml +90 -0
- package/cliproxy/config.template.yaml +70 -0
- package/cliproxy/docs/cloud-adoption.md +16 -0
- package/cliproxy/docs/deployment.png +0 -0
- package/cliproxy/docs/deployment.svg +109 -0
- package/cliproxy/docs/health-workflow.example.yml +185 -0
- package/cliproxy/docs/migration.md +131 -0
- package/cliproxy/docs/model-health-report.schema.json +364 -0
- package/cliproxy/docs/model-health.md +393 -0
- package/cliproxy/image.lock.json +5 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
- package/cliproxy/models.json +1618 -0
- package/cliproxy/requirements.lock +78 -0
- package/cliproxy/scripts/accounts.py +38 -0
- package/cliproxy/scripts/audit.py +123 -0
- package/cliproxy/scripts/backup +234 -0
- package/cliproxy/scripts/check.py +34 -0
- package/cliproxy/scripts/checkpoint.py +226 -0
- package/cliproxy/scripts/cli.py +64 -0
- package/cliproxy/scripts/cliproxy_host.py +257 -0
- package/cliproxy/scripts/cloud_state.py +150 -0
- package/cliproxy/scripts/container-health +6 -0
- package/cliproxy/scripts/docker +24 -0
- package/cliproxy/scripts/external-health.py +79 -0
- package/cliproxy/scripts/fleet_config.py +43 -0
- package/cliproxy/scripts/fleet_sync.py +274 -0
- package/cliproxy/scripts/initialize +84 -0
- package/cliproxy/scripts/install-dependencies +48 -0
- package/cliproxy/scripts/install.py +406 -0
- package/cliproxy/scripts/model_health.py +16 -0
- package/cliproxy/scripts/modelhealth/__init__.py +5 -0
- package/cliproxy/scripts/modelhealth/checks.py +429 -0
- package/cliproxy/scripts/modelhealth/cli.py +117 -0
- package/cliproxy/scripts/modelhealth/config.py +186 -0
- package/cliproxy/scripts/modelhealth/emulator.py +536 -0
- package/cliproxy/scripts/modelhealth/expect.py +88 -0
- package/cliproxy/scripts/modelhealth/findings.py +183 -0
- package/cliproxy/scripts/modelhealth/protocol.py +330 -0
- package/cliproxy/scripts/modelhealth/redact.py +28 -0
- package/cliproxy/scripts/modelhealth/report.py +150 -0
- package/cliproxy/scripts/modelhealth/runner.py +390 -0
- package/cliproxy/scripts/modelhealth/scenario.py +131 -0
- package/cliproxy/scripts/modelhealth/schema.py +63 -0
- package/cliproxy/scripts/modelhealth/simulate.py +111 -0
- package/cliproxy/scripts/modelhealth/transport.py +213 -0
- package/cliproxy/scripts/monitor +68 -0
- package/cliproxy/scripts/ops +527 -0
- package/cliproxy/scripts/public-endpoint +127 -0
- package/cliproxy/scripts/recover_node.py +177 -0
- package/cliproxy/scripts/restore-verify +60 -0
- package/cliproxy/scripts/retention.py +144 -0
- package/cliproxy/scripts/service-start +7 -0
- package/cliproxy/scripts/validate-config +56 -0
- package/cliproxy/scripts/vault-access.py +57 -0
- package/cliproxy/scripts/vault-backup-login +55 -0
- package/cliproxy/scripts/vault-recovery +56 -0
- package/cliproxy/scripts/vault-renew +30 -0
- package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
- package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
- package/cliproxy/systemd/cliproxy-backup.service +13 -0
- package/cliproxy/systemd/cliproxy-backup.timer +8 -0
- package/cliproxy/systemd/cliproxy-fleet.service +17 -0
- package/cliproxy/systemd/cliproxy-gc.service +13 -0
- package/cliproxy/systemd/cliproxy-gc.timer +8 -0
- package/cliproxy/systemd/cliproxy-monitor.service +8 -0
- package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
- package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
- package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
- package/cliproxy/vendor/LICENSE +22 -0
- package/cliproxy/vendor/config.upstream.yaml +1349 -0
- package/examples/fleet.json +95 -0
- package/examples/roster.json +13 -0
- package/mise.lock +1492 -0
- package/mise.toml +99 -0
- package/package.json +65 -4
- package/schema/fleet.schema.json +364 -0
- package/schema/roster.schema.json +77 -0
- package/src/agents/account-relay.ts +111 -0
- package/src/agents/accounts.ts +85 -0
- package/src/agents/audit.ts +77 -0
- package/src/agents/cli.ts +13 -0
- package/src/agents/command.ts +204 -0
- package/src/agents/converge.ts +123 -0
- package/src/agents/doctor.ts +107 -0
- package/src/agents/errors.ts +2 -0
- package/src/agents/files.ts +73 -0
- package/src/agents/fleet-verify.ts +156 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +226 -0
- package/src/agents/provider-errors.ts +50 -0
- package/src/agents/registry.ts +55 -0
- package/src/agents/render.ts +110 -0
- package/src/agents/round-trip.ts +192 -0
- package/src/agents/secrets.ts +13 -0
- package/src/agents/toolchain.ts +34 -0
- package/src/agents/verify-cache.ts +103 -0
- package/src/agents/verify.ts +127 -0
- package/src/auth-checks.ts +163 -0
- package/src/cli/agent-guide.ts +95 -0
- package/src/cli/app.ts +115 -0
- package/src/cli/cli.ts +21 -0
- package/src/cli/command-index.ts +50 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-sync-checkout.ts +91 -0
- package/src/cli/fleet-sync-enrollment.ts +144 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +193 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +314 -0
- package/src/cli/fleet-sync-privilege.ts +27 -0
- package/src/cli/fleet-sync-progress.ts +109 -0
- package/src/cli/fleet-sync-remote.ts +236 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +309 -0
- package/src/cli/fleet.ts +163 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +114 -0
- package/src/cli/node-t3.ts +31 -0
- package/src/cli/options.ts +141 -0
- package/src/cli/probe-source.ts +7 -0
- package/src/cli/program.ts +169 -0
- package/src/cli/register-fleet.ts +246 -0
- package/src/cli/registry.ts +158 -0
- package/src/cli/repair-apply.ts +67 -0
- package/src/cli/repair-guard.ts +11 -0
- package/src/cli/repair-inspect.ts +150 -0
- package/src/cli/repair-script.ts +76 -0
- package/src/cli/repair.ts +209 -0
- package/src/cli/roster.ts +89 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +64 -0
- package/src/cli/source-operations.ts +78 -0
- package/src/cli/t3/command.ts +90 -0
- package/src/cli/t3/register.ts +55 -0
- package/src/cli/tailnet.ts +127 -0
- package/src/cli/ui-actions.ts +133 -0
- package/src/cli/ui-controller.ts +113 -0
- package/src/cli/ui-data.ts +106 -0
- package/src/cli/ui-execute.ts +163 -0
- package/src/cli/ui-repairs.ts +34 -0
- package/src/cli/ui-reports.ts +211 -0
- package/src/cli/ui-targets.ts +100 -0
- package/src/cli/ui-terminal.ts +109 -0
- package/src/cli/ui-types.ts +49 -0
- package/src/cli/ui-wizard.ts +101 -0
- package/src/cli/ui.ts +9 -0
- package/src/cli/worker-health-output.ts +37 -0
- package/src/config.ts +186 -0
- package/src/configure.ts +255 -0
- package/src/definition.ts +260 -0
- package/src/diagnostics.ts +92 -0
- package/src/doctor-check.ts +10 -0
- package/src/errors.ts +1 -0
- package/src/fingerprint.ts +120 -0
- package/src/fleet-config.ts +88 -0
- package/src/index.ts +6 -0
- package/src/input-hashes.ts +73 -0
- package/src/machine/local-source.ts +52 -0
- package/src/machine/lock-error.ts +2 -0
- package/src/machine/platform.ts +44 -0
- package/src/machine/source.ts +68 -0
- package/src/machine/state.ts +185 -0
- package/src/machine-health.ts +95 -0
- package/src/managed-source.ts +47 -0
- package/src/manager-failure.ts +48 -0
- package/src/manager-sync.ts +400 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/apply.ts +171 -0
- package/src/monitoring/beszel.ts +191 -0
- package/src/monitoring/enroll.ts +110 -0
- package/src/monitoring/hub-access.ts +113 -0
- package/src/monitoring/hub-viewer.ts +142 -0
- package/src/monitoring/hub.ts +93 -0
- package/src/monitoring/manager-agent.ts +73 -0
- package/src/monitoring/manager-hub.ts +232 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +372 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +62 -0
- package/src/platform/darwin.ts +203 -0
- package/src/platform/detect.ts +11 -0
- package/src/platform/fallback.ts +110 -0
- package/src/platform/index.ts +38 -0
- package/src/platform/launchd.ts +98 -0
- package/src/platform/linux.ts +139 -0
- package/src/platform/ps.ts +45 -0
- package/src/platform/resource-verify.ts +128 -0
- package/src/platform/systemd-user.ts +95 -0
- package/src/platform/t3-policy.ts +137 -0
- package/src/platform/t3-route.ts +68 -0
- package/src/platform/t3-service.ts +75 -0
- package/src/platform/types.ts +123 -0
- package/src/platform/ubuntu-audit.ts +63 -0
- package/src/platform/ubuntu-verify.ts +105 -0
- package/src/platform/windows.ts +148 -0
- package/src/power-recovery.ts +55 -0
- package/src/probe.ts +51 -0
- package/src/protocol.ts +119 -0
- package/src/proxy-converge.ts +85 -0
- package/src/remote-bootstrap.ts +212 -0
- package/src/remote-entry.ts +89 -0
- package/src/remote-lock.ts +12 -0
- package/src/repair-catalog.ts +206 -0
- package/src/repair-plan.ts +78 -0
- package/src/repair-runner.ts +84 -0
- package/src/report.ts +71 -0
- package/src/runner.ts +104 -0
- package/src/secrets.ts +169 -0
- package/src/service-auth.ts +115 -0
- package/src/ssh.ts +135 -0
- package/src/sync.ts +104 -0
- package/src/t3/accounts.ts +81 -0
- package/src/t3/desired.ts +165 -0
- package/src/t3/files.ts +81 -0
- package/src/t3/names-check.ts +53 -0
- package/src/t3/names.ts +155 -0
- package/src/t3/plan.ts +82 -0
- package/src/t3/providers.ts +74 -0
- package/src/t3/reconcile.ts +123 -0
- package/src/t3-endpoint.ts +48 -0
- package/src/t3-linked-check.ts +101 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/test-fixtures.ts +36 -0
- package/src/toolchain-checks.ts +83 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +95 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +115 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +176 -0
- package/src/worker-policy.ts +64 -0
- package/src/worker-readiness.ts +64 -0
- package/src/workstation-checks.ts +29 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, writeFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { delimiter, join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import macos from '../../ansible/macos/policy.json';
|
|
6
|
+
import type { Runner, CommandOptions, CommandResult } from '../types';
|
|
7
|
+
|
|
8
|
+
/** Ubuntu provides ansible-core through APT; macOS runs the pinned release through
|
|
9
|
+
* uv's isolated tool runner so no global Python environment is modified.
|
|
10
|
+
*/
|
|
11
|
+
export function ansibleCommand(
|
|
12
|
+
path = process.env['PATH'] ?? '',
|
|
13
|
+
exists: (candidate: string) => boolean = existsSync
|
|
14
|
+
): string[] {
|
|
15
|
+
const found = path
|
|
16
|
+
.split(delimiter)
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.some((directory) => exists(join(directory, 'ansible-playbook')));
|
|
19
|
+
return found
|
|
20
|
+
? ['ansible-playbook']
|
|
21
|
+
: [
|
|
22
|
+
'uvx',
|
|
23
|
+
'--from',
|
|
24
|
+
`ansible-core==${macos.ansibleCore}`,
|
|
25
|
+
'ansible-playbook',
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Private fleet state on disk, never /tmp: a worker's /tmp is RAM-backed with a
|
|
30
|
+
* per-user quota that agent caches can exhaust, and the baseline that bounds it
|
|
31
|
+
* must still be able to run then.
|
|
32
|
+
*/
|
|
33
|
+
export function variablesDirectory(home = homedir()): string {
|
|
34
|
+
return join(home, '.local/state/personal/fleet/run');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Ansible resolves @file paths before opening them; @/dev/stdin is not portable
|
|
38
|
+
* across versions when stdin is a pipe. Keep runtime variables in a private,
|
|
39
|
+
* short-lived directory instead. Never put their values in argv or logs.
|
|
40
|
+
*/
|
|
41
|
+
export async function runAnsible(
|
|
42
|
+
run: Runner,
|
|
43
|
+
playbook: string,
|
|
44
|
+
options: CommandOptions & { input: string },
|
|
45
|
+
scratch = variablesDirectory()
|
|
46
|
+
): Promise<CommandResult> {
|
|
47
|
+
if (Buffer.byteLength(options.input) > 256 * 1024)
|
|
48
|
+
throw new Error('ANSIBLE_VARIABLE_LIMIT');
|
|
49
|
+
await mkdir(scratch, { recursive: true, mode: 0o700 });
|
|
50
|
+
const directory = await mkdtemp(join(scratch, 'personal-ansible-'));
|
|
51
|
+
const path = join(directory, 'variables.json');
|
|
52
|
+
try {
|
|
53
|
+
await writeFile(path, options.input, { mode: 0o600, flag: 'wx' });
|
|
54
|
+
const { input: _input, ...commandOptions } = options;
|
|
55
|
+
return await run(
|
|
56
|
+
[...ansibleCommand(), playbook, '--extra-vars', '@' + path],
|
|
57
|
+
commandOptions
|
|
58
|
+
);
|
|
59
|
+
} finally {
|
|
60
|
+
await rm(directory, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
NotificationKind,
|
|
8
|
+
NotifierCapability,
|
|
9
|
+
NotifierTestResult,
|
|
10
|
+
Platform,
|
|
11
|
+
ProcessInfo,
|
|
12
|
+
ScheduledJob,
|
|
13
|
+
ScheduledJobStatus,
|
|
14
|
+
} from './types';
|
|
15
|
+
import {
|
|
16
|
+
installLaunchdJob,
|
|
17
|
+
launchdJobStatus,
|
|
18
|
+
removeLaunchdJob,
|
|
19
|
+
} from './launchd';
|
|
20
|
+
import { psProcesses } from './ps';
|
|
21
|
+
|
|
22
|
+
function appleScriptString(value: string): string {
|
|
23
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\r?\n/g, '\\n')}"`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class DarwinPlatform implements Platform {
|
|
27
|
+
readonly dockerSetupScript = 'bootstrap/docker-desktop.sh';
|
|
28
|
+
readonly name = 'darwin' as const;
|
|
29
|
+
readonly label = 'macOS';
|
|
30
|
+
|
|
31
|
+
opencodeDir(): string {
|
|
32
|
+
return join(homedir(), '.config/opencode');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
cacheDir(): string {
|
|
36
|
+
return join(homedir(), '.cache');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
globalBinDir(): string {
|
|
40
|
+
return join(homedir(), '.local/bin');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
canBuildSwiftNotifier(): boolean {
|
|
44
|
+
return (
|
|
45
|
+
spawnSync('/usr/bin/which', ['swiftc'], { stdio: 'ignore' }).status === 0
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
shellRcFiles(): readonly string[] {
|
|
50
|
+
const home = homedir();
|
|
51
|
+
return [
|
|
52
|
+
join(home, '.zshrc'),
|
|
53
|
+
join(home, '.bashrc'),
|
|
54
|
+
join(home, '.bash_profile'),
|
|
55
|
+
join(home, '.config/fish/config.fish'),
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
notifierCapabilities(): NotifierCapability {
|
|
60
|
+
return { native: true, sounds: true, clickToFocus: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async postNotification(input: {
|
|
64
|
+
title: string;
|
|
65
|
+
message: string;
|
|
66
|
+
kind: NotificationKind;
|
|
67
|
+
}): Promise<NotifierTestResult> {
|
|
68
|
+
const cli = join(this.opencodeDir(), 'bin/opencode-notifier');
|
|
69
|
+
try {
|
|
70
|
+
const { existsSync } = await import('node:fs');
|
|
71
|
+
if (existsSync(cli)) {
|
|
72
|
+
const body = JSON.stringify({
|
|
73
|
+
kind: input.kind,
|
|
74
|
+
title: input.title,
|
|
75
|
+
message: input.message,
|
|
76
|
+
subtitle: 'dev-fleet test',
|
|
77
|
+
sessionID: 'ses_ws_test',
|
|
78
|
+
directory: process.cwd(),
|
|
79
|
+
sessionTitle: 'dev-fleet test',
|
|
80
|
+
token: '',
|
|
81
|
+
});
|
|
82
|
+
const r = spawnSync(cli, ['--post', body], { encoding: 'utf8' });
|
|
83
|
+
if (r.status === 0) {
|
|
84
|
+
return { ok: true, detail: 'posted via OpenCodeNotifier daemon' };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Fall through to osascript.
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const script = `display notification ${appleScriptString(input.message)} with title ${appleScriptString(input.title)}`;
|
|
92
|
+
execSync(`osascript -e ${JSON.stringify(script)}`, { stdio: 'ignore' });
|
|
93
|
+
return { ok: true, detail: 'posted via osascript fallback' };
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
return { ok: false, detail: `osascript failed: ${msg}` };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
isNotifierDaemonRunning(): boolean {
|
|
101
|
+
try {
|
|
102
|
+
const out = execSync('pgrep -fl "OpenCodeNotifier.*daemon"', {
|
|
103
|
+
encoding: 'utf8',
|
|
104
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
105
|
+
});
|
|
106
|
+
return out.trim().length > 0;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
restartNotifierDaemon(): void {
|
|
113
|
+
spawnSync('pkill', ['-f', 'OpenCodeNotifier.*--daemon'], {
|
|
114
|
+
stdio: 'ignore',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
availableSystemSounds(): string[] {
|
|
119
|
+
const dirs = [
|
|
120
|
+
'/System/Library/Sounds',
|
|
121
|
+
'/Library/Sounds',
|
|
122
|
+
join(homedir(), 'Library/Sounds'),
|
|
123
|
+
];
|
|
124
|
+
const names = new Set<string>();
|
|
125
|
+
for (const dir of dirs) {
|
|
126
|
+
let entries: string[] = [];
|
|
127
|
+
try {
|
|
128
|
+
entries = readdirSync(dir);
|
|
129
|
+
} catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const m = entry.match(/^(.*)\.(aiff?|wav|mp3|m4a|caf)$/i);
|
|
134
|
+
if (m?.[1]) names.add(m[1]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [...names].sort((a, b) => a.localeCompare(b));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async playSystemSound(name: string): Promise<NotifierTestResult> {
|
|
141
|
+
const trimmed = name.trim();
|
|
142
|
+
if (trimmed.length === 0) {
|
|
143
|
+
return { ok: false, detail: 'empty sound name' };
|
|
144
|
+
}
|
|
145
|
+
const dirs = [
|
|
146
|
+
'/System/Library/Sounds',
|
|
147
|
+
'/Library/Sounds',
|
|
148
|
+
join(homedir(), 'Library/Sounds'),
|
|
149
|
+
];
|
|
150
|
+
const exts = ['aiff', 'aif', 'wav', 'mp3', 'm4a', 'caf'];
|
|
151
|
+
for (const dir of dirs) {
|
|
152
|
+
for (const ext of exts) {
|
|
153
|
+
const candidate = join(dir, `${trimmed}.${ext}`);
|
|
154
|
+
if (existsSync(candidate)) {
|
|
155
|
+
const r = spawnSync('afplay', [candidate], { stdio: 'ignore' });
|
|
156
|
+
if (r.status === 0) {
|
|
157
|
+
return { ok: true, detail: `played ${trimmed} via afplay` };
|
|
158
|
+
}
|
|
159
|
+
return { ok: false, detail: `afplay failed for ${trimmed}` };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
detail: `unknown sound "${trimmed}" (see \`devfleet workstation opencode notifications sounds list\`)`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
listProcesses(): ProcessInfo[] | null {
|
|
170
|
+
return psProcesses();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
tailscaleAppBinaries(): readonly string[] {
|
|
174
|
+
return ['/Applications/Tailscale.app/Contents/MacOS/Tailscale'].filter(
|
|
175
|
+
(p) => existsSync(p)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
vscodeSettingsPath(): string | null {
|
|
180
|
+
return join(
|
|
181
|
+
homedir(),
|
|
182
|
+
'Library/Application Support/Code/User/settings.json'
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
vscodeCliBinaries(): readonly string[] {
|
|
187
|
+
return [
|
|
188
|
+
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
|
|
189
|
+
].filter((p) => existsSync(p));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
installScheduledJob(job: ScheduledJob): NotifierTestResult {
|
|
193
|
+
return installLaunchdJob(job);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
removeScheduledJob(label: string): NotifierTestResult {
|
|
197
|
+
return removeLaunchdJob(label);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
scheduledJobStatus(label: string): ScheduledJobStatus {
|
|
201
|
+
return launchdJobStatus(label);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PlatformName } from './types';
|
|
2
|
+
|
|
3
|
+
/** Map `process.platform` onto the dev-fleet platform names. Pure — unit-tested. */
|
|
4
|
+
export function detectPlatformName(
|
|
5
|
+
platform: NodeJS.Platform = process.platform
|
|
6
|
+
): PlatformName {
|
|
7
|
+
if (platform === 'darwin') return 'darwin';
|
|
8
|
+
if (platform === 'linux') return 'linux';
|
|
9
|
+
if (platform === 'win32') return 'windows';
|
|
10
|
+
return 'unknown';
|
|
11
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
NotificationKind,
|
|
6
|
+
NotifierCapability,
|
|
7
|
+
NotifierTestResult,
|
|
8
|
+
Platform,
|
|
9
|
+
ProcessInfo,
|
|
10
|
+
ScheduledJob,
|
|
11
|
+
ScheduledJobStatus,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
/** Safe default for unrecognized platforms — filesystem converges, notify degrades. */
|
|
15
|
+
export class FallbackPlatform implements Platform {
|
|
16
|
+
readonly name = 'unknown' as const;
|
|
17
|
+
readonly label = 'Unknown';
|
|
18
|
+
|
|
19
|
+
opencodeDir(): string {
|
|
20
|
+
return join(homedir(), '.config/opencode');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
cacheDir(): string {
|
|
24
|
+
return join(homedir(), '.cache');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
globalBinDir(): string {
|
|
28
|
+
return join(homedir(), '.local/bin');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
canBuildSwiftNotifier(): boolean {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
shellRcFiles(): readonly string[] {
|
|
36
|
+
return [join(homedir(), '.profile')];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
notifierCapabilities(): NotifierCapability {
|
|
40
|
+
return {
|
|
41
|
+
native: false,
|
|
42
|
+
sounds: false,
|
|
43
|
+
clickToFocus: false,
|
|
44
|
+
reason: 'unrecognized platform — notifications disabled',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async postNotification(_input: {
|
|
49
|
+
title: string;
|
|
50
|
+
message: string;
|
|
51
|
+
kind: NotificationKind;
|
|
52
|
+
}): Promise<NotifierTestResult> {
|
|
53
|
+
return { ok: false, detail: 'notifications unsupported on this platform' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
isNotifierDaemonRunning(): boolean {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
restartNotifierDaemon(): void {
|
|
61
|
+
// No-op.
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
availableSystemSounds(): string[] {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async playSystemSound(name: string): Promise<NotifierTestResult> {
|
|
69
|
+
void name;
|
|
70
|
+
return { ok: false, detail: 'sounds unsupported on this platform' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
listProcesses(): ProcessInfo[] | null {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
tailscaleAppBinaries(): readonly string[] {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
vscodeSettingsPath(): string | null {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
vscodeCliBinaries(): readonly string[] {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
installScheduledJob(job: ScheduledJob): NotifierTestResult {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
detail: `scheduled jobs are not supported on ${this.label} (${job.label})`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
removeScheduledJob(label: string): NotifierTestResult {
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
detail: `nothing to remove for ${label} on ${this.label}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
scheduledJobStatus(label: string): ScheduledJobStatus {
|
|
104
|
+
return {
|
|
105
|
+
supported: false,
|
|
106
|
+
installed: false,
|
|
107
|
+
detail: `not supported on ${this.label} (${label})`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DarwinPlatform } from './darwin';
|
|
2
|
+
import { detectPlatformName } from './detect';
|
|
3
|
+
import { FallbackPlatform } from './fallback';
|
|
4
|
+
import { LinuxPlatform } from './linux';
|
|
5
|
+
import { WindowsPlatform } from './windows';
|
|
6
|
+
import type { Platform, PlatformName } from './types';
|
|
7
|
+
|
|
8
|
+
export type { Platform, PlatformName } from './types';
|
|
9
|
+
export type {
|
|
10
|
+
NotificationKind,
|
|
11
|
+
NotifierCapability,
|
|
12
|
+
NotifierTestResult,
|
|
13
|
+
GlobalInstallResult,
|
|
14
|
+
} from './types';
|
|
15
|
+
export { detectPlatformName } from './detect';
|
|
16
|
+
|
|
17
|
+
/** Polymorphic factory — callers never branch on `process.platform` directly. */
|
|
18
|
+
export function getPlatform(name?: PlatformName): Platform {
|
|
19
|
+
const resolved = name ?? detectPlatformName();
|
|
20
|
+
switch (resolved) {
|
|
21
|
+
case 'darwin':
|
|
22
|
+
return new DarwinPlatform();
|
|
23
|
+
case 'linux':
|
|
24
|
+
return new LinuxPlatform();
|
|
25
|
+
case 'windows':
|
|
26
|
+
return new WindowsPlatform();
|
|
27
|
+
case 'unknown':
|
|
28
|
+
return new FallbackPlatform();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let cached: Platform | null = null;
|
|
33
|
+
|
|
34
|
+
/** Memoized current-platform adapter. */
|
|
35
|
+
export function currentPlatform(): Platform {
|
|
36
|
+
if (cached === null) cached = getPlatform();
|
|
37
|
+
return cached;
|
|
38
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir, userInfo } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
NotifierTestResult,
|
|
8
|
+
ScheduledJob,
|
|
9
|
+
ScheduledJobStatus,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
function xml(value: string): string {
|
|
13
|
+
return value
|
|
14
|
+
.replace(/&/g, '&')
|
|
15
|
+
.replace(/</g, '<')
|
|
16
|
+
.replace(/>/g, '>');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function launchAgentPath(label: string, home = homedir()): string {
|
|
20
|
+
return join(home, 'Library/LaunchAgents', `${label}.plist`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** launchd agent plist for a periodic job. Pure — unit-tested. */
|
|
24
|
+
export function launchdPlist(job: ScheduledJob): string {
|
|
25
|
+
const args = job.command.map((a) => ` <string>${xml(a)}</string>`);
|
|
26
|
+
const env = Object.entries(job.env).map(
|
|
27
|
+
([k, v]) => ` <key>${xml(k)}</key><string>${xml(v)}</string>`
|
|
28
|
+
);
|
|
29
|
+
return [
|
|
30
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
31
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
32
|
+
'<plist version="1.0"><dict>',
|
|
33
|
+
` <key>Label</key><string>${xml(job.label)}</string>`,
|
|
34
|
+
' <key>ProgramArguments</key><array>',
|
|
35
|
+
...args,
|
|
36
|
+
' </array>',
|
|
37
|
+
' <key>EnvironmentVariables</key><dict>',
|
|
38
|
+
...env,
|
|
39
|
+
' </dict>',
|
|
40
|
+
` <key>StartInterval</key><integer>${job.intervalSeconds}</integer>`,
|
|
41
|
+
' <key>RunAtLoad</key><true/>',
|
|
42
|
+
' <key>LowPriorityIO</key><true/>',
|
|
43
|
+
' <key>Nice</key><integer>10</integer>',
|
|
44
|
+
` <key>StandardOutPath</key><string>${xml(join(job.logDir, `${job.label}.out.log`))}</string>`,
|
|
45
|
+
` <key>StandardErrorPath</key><string>${xml(join(job.logDir, `${job.label}.err.log`))}</string>`,
|
|
46
|
+
'</dict></plist>',
|
|
47
|
+
'',
|
|
48
|
+
].join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function domain(): string {
|
|
52
|
+
return `gui/${userInfo().uid}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function installLaunchdJob(job: ScheduledJob): NotifierTestResult {
|
|
56
|
+
const plist = launchAgentPath(job.label);
|
|
57
|
+
mkdirSync(join(homedir(), 'Library/LaunchAgents'), { recursive: true });
|
|
58
|
+
mkdirSync(job.logDir, { recursive: true });
|
|
59
|
+
writeFileSync(plist, launchdPlist(job));
|
|
60
|
+
spawnSync('launchctl', ['bootout', `${domain()}/${job.label}`], {
|
|
61
|
+
stdio: 'ignore',
|
|
62
|
+
});
|
|
63
|
+
const r = spawnSync('launchctl', ['bootstrap', domain(), plist], {
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
});
|
|
66
|
+
return r.status === 0
|
|
67
|
+
? { ok: true, detail: `launchd agent ${plist}` }
|
|
68
|
+
: { ok: false, detail: `launchctl bootstrap failed: ${r.stderr.trim()}` };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function removeLaunchdJob(label: string): NotifierTestResult {
|
|
72
|
+
spawnSync('launchctl', ['bootout', `${domain()}/${label}`], {
|
|
73
|
+
stdio: 'ignore',
|
|
74
|
+
});
|
|
75
|
+
rmSync(launchAgentPath(label), { force: true });
|
|
76
|
+
return { ok: true, detail: `removed launchd agent ${label}` };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function launchdJobStatus(label: string): ScheduledJobStatus {
|
|
80
|
+
const r = spawnSync('launchctl', ['print', `${domain()}/${label}`], {
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
});
|
|
83
|
+
if (r.status !== 0) {
|
|
84
|
+
const present = existsSync(launchAgentPath(label));
|
|
85
|
+
return {
|
|
86
|
+
supported: true,
|
|
87
|
+
installed: false,
|
|
88
|
+
detail: present ? 'plist present but not loaded' : 'not installed',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const lastExit =
|
|
92
|
+
/last exit code = (.+)/.exec(r.stdout)?.[1]?.trim() ?? 'unknown';
|
|
93
|
+
return {
|
|
94
|
+
supported: true,
|
|
95
|
+
installed: true,
|
|
96
|
+
detail: `loaded; last exit code ${lastExit}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
NotificationKind,
|
|
7
|
+
NotifierCapability,
|
|
8
|
+
NotifierTestResult,
|
|
9
|
+
Platform,
|
|
10
|
+
ProcessInfo,
|
|
11
|
+
ScheduledJob,
|
|
12
|
+
ScheduledJobStatus,
|
|
13
|
+
} from './types';
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import {
|
|
16
|
+
installSystemdJob,
|
|
17
|
+
removeSystemdJob,
|
|
18
|
+
systemdJobStatus,
|
|
19
|
+
} from './systemd-user';
|
|
20
|
+
import { psProcesses } from './ps';
|
|
21
|
+
|
|
22
|
+
export class LinuxPlatform implements Platform {
|
|
23
|
+
readonly name = 'linux' as const;
|
|
24
|
+
readonly label = 'Linux';
|
|
25
|
+
|
|
26
|
+
opencodeDir(): string {
|
|
27
|
+
const base =
|
|
28
|
+
process.env['XDG_CONFIG_HOME']?.trim() || join(homedir(), '.config');
|
|
29
|
+
return join(base, 'opencode');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
cacheDir(): string {
|
|
33
|
+
const base =
|
|
34
|
+
process.env['XDG_CACHE_HOME']?.trim() || join(homedir(), '.cache');
|
|
35
|
+
return base;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
globalBinDir(): string {
|
|
39
|
+
return join(homedir(), '.local/bin');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
canBuildSwiftNotifier(): boolean {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
shellRcFiles(): readonly string[] {
|
|
47
|
+
const home = homedir();
|
|
48
|
+
return [
|
|
49
|
+
join(home, '.bashrc'),
|
|
50
|
+
join(home, '.bash_profile'),
|
|
51
|
+
join(home, '.zshrc'),
|
|
52
|
+
join(home, '.config/fish/config.fish'),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
notifierCapabilities(): NotifierCapability {
|
|
57
|
+
return {
|
|
58
|
+
native: true,
|
|
59
|
+
sounds: false,
|
|
60
|
+
clickToFocus: false,
|
|
61
|
+
reason:
|
|
62
|
+
'Linux uses notify-send (no sounds, no click-to-focus); the macOS OpenCodeNotifier app is macOS-only.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async postNotification(input: {
|
|
67
|
+
title: string;
|
|
68
|
+
message: string;
|
|
69
|
+
kind: NotificationKind;
|
|
70
|
+
}): Promise<NotifierTestResult> {
|
|
71
|
+
void input.kind;
|
|
72
|
+
const candidates: ReadonlyArray<{ cmd: string; args: string[] }> = [
|
|
73
|
+
{ cmd: 'notify-send', args: [input.title, input.message] },
|
|
74
|
+
{ cmd: 'zenity', args: ['--notification', `--text=${input.message}`] },
|
|
75
|
+
];
|
|
76
|
+
for (const candidate of candidates) {
|
|
77
|
+
const r = spawnSync(candidate.cmd, candidate.args, { stdio: 'ignore' });
|
|
78
|
+
if (r.status === 0) {
|
|
79
|
+
return { ok: true, detail: `posted via ${candidate.cmd}` };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
detail: 'no notifier found (install libnotify: notify-send)',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
isNotifierDaemonRunning(): boolean {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
restartNotifierDaemon(): void {
|
|
93
|
+
// No daemon on Linux — no-op.
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
availableSystemSounds(): string[] {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async playSystemSound(name: string): Promise<NotifierTestResult> {
|
|
101
|
+
void name;
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
detail: 'sounds unsupported on Linux (notify-send has no sound API)',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
listProcesses(): ProcessInfo[] | null {
|
|
109
|
+
return psProcesses();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
tailscaleAppBinaries(): readonly string[] {
|
|
113
|
+
return ['/usr/bin/tailscale', '/usr/sbin/tailscale'].filter((p) =>
|
|
114
|
+
existsSync(p)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
vscodeSettingsPath(): string | null {
|
|
119
|
+
const base =
|
|
120
|
+
process.env['XDG_CONFIG_HOME']?.trim() || join(homedir(), '.config');
|
|
121
|
+
return join(base, 'Code/User/settings.json');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
vscodeCliBinaries(): readonly string[] {
|
|
125
|
+
return ['/usr/share/code/bin/code'].filter((p) => existsSync(p));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
installScheduledJob(job: ScheduledJob): NotifierTestResult {
|
|
129
|
+
return installSystemdJob(job);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
removeScheduledJob(label: string): NotifierTestResult {
|
|
133
|
+
return removeSystemdJob(label);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
scheduledJobStatus(label: string): ScheduledJobStatus {
|
|
137
|
+
return systemdJobStatus(label);
|
|
138
|
+
}
|
|
139
|
+
}
|