@playdrop/playdrop-cli 0.10.6 → 0.10.7
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/README.md +1 -1
- package/config/client-meta.json +1 -1
- package/dist/commands/agents.js +15 -0
- package/dist/commands/upgrade.d.ts +10 -1
- package/dist/commands/upgrade.js +99 -9
- package/dist/commands/worker.d.ts +64 -0
- package/dist/commands/worker.js +873 -10
- package/dist/index.js +41 -1
- package/node_modules/@playdrop/api-client/dist/client.d.ts +3 -2
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +3 -2
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +23 -12
- package/node_modules/@playdrop/api-client/dist/index.d.ts +3 -2
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +10 -5
- package/node_modules/@playdrop/config/client-meta.json +1 -1
- package/node_modules/@playdrop/types/dist/api.d.ts +108 -9
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/api.js +54 -3
- package/package.json +1 -1
package/dist/commands/worker.js
CHANGED
|
@@ -35,6 +35,11 @@ exports.assertWorkerProjectVersionBumped = assertWorkerProjectVersionBumped;
|
|
|
35
35
|
exports.buildAgentFailureCode = buildAgentFailureCode;
|
|
36
36
|
exports.describeAgentFailureForEvent = describeAgentFailureForEvent;
|
|
37
37
|
exports.createLocalPlaydropShim = createLocalPlaydropShim;
|
|
38
|
+
exports.readActivePersonalWorkerRuntimeState = readActivePersonalWorkerRuntimeState;
|
|
39
|
+
exports.parseLaunchAgentProgramArguments = parseLaunchAgentProgramArguments;
|
|
40
|
+
exports.assertLaunchAgentPlistCompatible = assertLaunchAgentPlistCompatible;
|
|
41
|
+
exports.showWorkerStatus = showWorkerStatus;
|
|
42
|
+
exports.setupWorker = setupWorker;
|
|
38
43
|
exports.startWorker = startWorker;
|
|
39
44
|
exports.reportTask = reportTask;
|
|
40
45
|
exports.reportCatalogueTask = reportCatalogueTask;
|
|
@@ -59,6 +64,7 @@ const output_1 = require("../output");
|
|
|
59
64
|
const shellProbe_1 = require("../shellProbe");
|
|
60
65
|
const upload_1 = require("./upload");
|
|
61
66
|
const review_1 = require("./review");
|
|
67
|
+
const agents_1 = require("./agents");
|
|
62
68
|
const runtime_1 = require("./worker/runtime");
|
|
63
69
|
var runtime_2 = require("./worker/runtime");
|
|
64
70
|
Object.defineProperty(exports, "DEFAULT_CODEX_TIMEOUT_MS", { enumerable: true, get: function () { return runtime_2.DEFAULT_CODEX_TIMEOUT_MS; } });
|
|
@@ -83,6 +89,7 @@ const CLAIM_BACKOFF_JITTER_MS = 500;
|
|
|
83
89
|
const FAIL_REPORT_RETRY_DELAY_MS = 2000;
|
|
84
90
|
const WORKER_TRANSCRIPT_CHUNK_BATCH_SIZE = 100;
|
|
85
91
|
const SLACK_API_BASE_URL = 'https://slack.com/api';
|
|
92
|
+
const WORKER_WRAPPER_CONTRACT_VERSION = 1;
|
|
86
93
|
const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'GAME_UPDATE', 'GAME_REVIEW'];
|
|
87
94
|
const requireFromWorker = (0, node_module_1.createRequire)(__filename);
|
|
88
95
|
const PLAYDROP_PLUGIN_DAEMON_GAME_CREATION_SKILL_PATH = 'skills/daemon-game-creation/SKILL.md';
|
|
@@ -209,7 +216,7 @@ function normalizeAssignmentWorkspaceFolder(value) {
|
|
|
209
216
|
return folderName;
|
|
210
217
|
}
|
|
211
218
|
function normalizeAssignmentAgent(value) {
|
|
212
|
-
if (value !== 'CODEX' && value !== 'CLAUDE_CODE') {
|
|
219
|
+
if (value !== 'CODEX' && value !== 'CLAUDE_CODE' && value !== 'CURSOR_COMPOSER') {
|
|
213
220
|
throw new Error('agent_task_assignment_agent_missing');
|
|
214
221
|
}
|
|
215
222
|
return value;
|
|
@@ -999,6 +1006,8 @@ function buildWorkerCapabilities(input) {
|
|
|
999
1006
|
const codexAuthenticated = typeof input === 'string' ? true : input.codexAuthenticated === true;
|
|
1000
1007
|
const claudeVersion = typeof input === 'string' ? null : input.claudeVersion?.trim() || null;
|
|
1001
1008
|
const claudeAuthenticated = typeof input === 'string' ? false : input.claudeAuthenticated === true;
|
|
1009
|
+
const cursorVersion = typeof input === 'string' ? null : input.cursorVersion?.trim() || null;
|
|
1010
|
+
const cursorAuthenticated = typeof input === 'string' ? false : input.cursorAuthenticated === true;
|
|
1002
1011
|
const npmVersion = typeof input === 'string' ? null : input.npmVersion?.trim() || null;
|
|
1003
1012
|
const playwright = typeof input === 'string' ? null : input.playwright ?? null;
|
|
1004
1013
|
const maxParallelTasks = typeof input === 'string'
|
|
@@ -1031,8 +1040,21 @@ function buildWorkerCapabilities(input) {
|
|
|
1031
1040
|
ready: claudeAuthenticated,
|
|
1032
1041
|
});
|
|
1033
1042
|
}
|
|
1043
|
+
if (cursorVersion) {
|
|
1044
|
+
degradedReasons.push('cursor_runner_not_configured');
|
|
1045
|
+
if (!cursorAuthenticated) {
|
|
1046
|
+
degradedReasons.push('cursor_not_authenticated');
|
|
1047
|
+
}
|
|
1048
|
+
agents.push({
|
|
1049
|
+
agent: 'CURSOR_COMPOSER',
|
|
1050
|
+
cliVersion: cursorVersion,
|
|
1051
|
+
authenticated: cursorAuthenticated,
|
|
1052
|
+
models: [],
|
|
1053
|
+
ready: false,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1034
1056
|
if (agents.length === 0) {
|
|
1035
|
-
throw new Error('agent_cli_not_found: install and authenticate Codex
|
|
1057
|
+
throw new Error('agent_cli_not_found: install and authenticate Codex, Claude Code, or Cursor before starting a PlayDrop worker.');
|
|
1036
1058
|
}
|
|
1037
1059
|
return {
|
|
1038
1060
|
agent: agents[0].agent,
|
|
@@ -1044,6 +1066,7 @@ function buildWorkerCapabilities(input) {
|
|
|
1044
1066
|
agents,
|
|
1045
1067
|
ready: agents.some((agent) => agent.ready === true),
|
|
1046
1068
|
degradedReasons,
|
|
1069
|
+
wrapperContractVersion: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_WRAPPER_CONTRACT_VERSION', WORKER_WRAPPER_CONTRACT_VERSION),
|
|
1047
1070
|
...(npmVersion ? { npmVersion } : {}),
|
|
1048
1071
|
...(playwright ? { playwright } : {}),
|
|
1049
1072
|
os: node_process_1.default.platform,
|
|
@@ -1655,10 +1678,762 @@ function probeClaudeInstallation() {
|
|
|
1655
1678
|
}
|
|
1656
1679
|
return { claudeVersion: match[1] };
|
|
1657
1680
|
}
|
|
1681
|
+
function probeCursorInstallation() {
|
|
1682
|
+
if (!(0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_ENABLE_CURSOR', false)) {
|
|
1683
|
+
return null;
|
|
1684
|
+
}
|
|
1685
|
+
const command = node_process_1.default.env.PLAYDROP_WORKER_CURSOR_COMMAND?.trim() || 'cursor';
|
|
1686
|
+
const probe = (0, shellProbe_1.runShell)((0, shellProbe_1.buildCommandPathProbe)(command));
|
|
1687
|
+
if (probe.exitCode !== 0 || !probe.output.trim()) {
|
|
1688
|
+
throw new Error(`worker_preflight_cursor_missing: ${command} was not found. Disable PLAYDROP_WORKER_ENABLE_CURSOR or install the configured Cursor command.`);
|
|
1689
|
+
}
|
|
1690
|
+
const versionResult = (0, shellProbe_1.runShell)(`${command} --version`);
|
|
1691
|
+
const version = versionResult.output.trim().split(/\r?\n/)[0]?.trim() || '';
|
|
1692
|
+
if (!version || versionResult.exitCode !== 0) {
|
|
1693
|
+
throw new Error(`cursor_version_check_failed: "${command} --version" exited with code ${versionResult.exitCode} and reported no version. Output: ${versionResult.output.slice(0, 200)}`);
|
|
1694
|
+
}
|
|
1695
|
+
return { cursorVersion: version };
|
|
1696
|
+
}
|
|
1658
1697
|
function readClaudeAuthenticated() {
|
|
1659
1698
|
const status = (0, shellProbe_1.runShell)('claude auth status --text');
|
|
1660
1699
|
return status.exitCode === 0;
|
|
1661
1700
|
}
|
|
1701
|
+
function readCursorAuthenticated() {
|
|
1702
|
+
return (0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_CURSOR_AUTHENTICATED', false);
|
|
1703
|
+
}
|
|
1704
|
+
function errorMessage(error) {
|
|
1705
|
+
return error instanceof Error ? error.message : String(error);
|
|
1706
|
+
}
|
|
1707
|
+
function errorCode(error) {
|
|
1708
|
+
return errorMessage(error).split(':')[0]?.trim() || 'unknown_error';
|
|
1709
|
+
}
|
|
1710
|
+
function readPlaydropPluginVersion(pluginRoot) {
|
|
1711
|
+
for (const relativePath of ['.codex-plugin/plugin.json', 'plugin.json', 'package.json']) {
|
|
1712
|
+
try {
|
|
1713
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(pluginRoot, relativePath), 'utf8'));
|
|
1714
|
+
const version = typeof parsed.version === 'string' ? parsed.version.trim() : '';
|
|
1715
|
+
if (version) {
|
|
1716
|
+
return version;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
catch {
|
|
1720
|
+
continue;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
function collectWorkerLocalStatus() {
|
|
1726
|
+
const degradedReasons = [];
|
|
1727
|
+
let codexVersion = null;
|
|
1728
|
+
let claudeVersion = null;
|
|
1729
|
+
let cursorVersion = null;
|
|
1730
|
+
let npmVersion = null;
|
|
1731
|
+
let playwright = null;
|
|
1732
|
+
let pluginRoot = null;
|
|
1733
|
+
let pluginVersion = null;
|
|
1734
|
+
let pluginError = null;
|
|
1735
|
+
try {
|
|
1736
|
+
codexVersion = probeCodexInstallation()?.codexVersion ?? null;
|
|
1737
|
+
}
|
|
1738
|
+
catch (error) {
|
|
1739
|
+
degradedReasons.push(errorCode(error));
|
|
1740
|
+
}
|
|
1741
|
+
try {
|
|
1742
|
+
claudeVersion = probeClaudeInstallation()?.claudeVersion ?? null;
|
|
1743
|
+
}
|
|
1744
|
+
catch (error) {
|
|
1745
|
+
degradedReasons.push(errorCode(error));
|
|
1746
|
+
}
|
|
1747
|
+
try {
|
|
1748
|
+
cursorVersion = probeCursorInstallation()?.cursorVersion ?? null;
|
|
1749
|
+
}
|
|
1750
|
+
catch (error) {
|
|
1751
|
+
degradedReasons.push(errorCode(error));
|
|
1752
|
+
}
|
|
1753
|
+
try {
|
|
1754
|
+
npmVersion = probeNpmVersion();
|
|
1755
|
+
}
|
|
1756
|
+
catch (error) {
|
|
1757
|
+
degradedReasons.push(errorCode(error));
|
|
1758
|
+
}
|
|
1759
|
+
try {
|
|
1760
|
+
playwright = probePlaywrightChromium();
|
|
1761
|
+
}
|
|
1762
|
+
catch (error) {
|
|
1763
|
+
degradedReasons.push(errorCode(error));
|
|
1764
|
+
}
|
|
1765
|
+
try {
|
|
1766
|
+
pluginRoot = resolvePlaydropPluginRoot();
|
|
1767
|
+
pluginVersion = readPlaydropPluginVersion(pluginRoot);
|
|
1768
|
+
}
|
|
1769
|
+
catch (error) {
|
|
1770
|
+
pluginError = errorMessage(error);
|
|
1771
|
+
degradedReasons.push(errorCode(error));
|
|
1772
|
+
}
|
|
1773
|
+
let capabilities = null;
|
|
1774
|
+
try {
|
|
1775
|
+
capabilities = buildWorkerCapabilities({
|
|
1776
|
+
codexVersion,
|
|
1777
|
+
codexAuthenticated: codexVersion ? readCodexAuthenticated() : false,
|
|
1778
|
+
claudeVersion,
|
|
1779
|
+
claudeAuthenticated: claudeVersion ? readClaudeAuthenticated() : false,
|
|
1780
|
+
cursorVersion,
|
|
1781
|
+
cursorAuthenticated: cursorVersion ? readCursorAuthenticated() : false,
|
|
1782
|
+
npmVersion,
|
|
1783
|
+
playwright,
|
|
1784
|
+
runningTaskCount: 0,
|
|
1785
|
+
maxParallelTasks: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_MAX_PARALLEL_TASKS', DEFAULT_WORKER_MAX_PARALLEL_TASKS),
|
|
1786
|
+
});
|
|
1787
|
+
if (pluginVersion) {
|
|
1788
|
+
capabilities.pluginVersion = pluginVersion;
|
|
1789
|
+
for (const agent of capabilities.agents) {
|
|
1790
|
+
agent.pluginVersion = pluginVersion;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
for (const reason of capabilities.degradedReasons ?? []) {
|
|
1794
|
+
degradedReasons.push(reason);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
catch (error) {
|
|
1798
|
+
degradedReasons.push(errorCode(error));
|
|
1799
|
+
}
|
|
1800
|
+
const uniqueReasons = Array.from(new Set(degradedReasons));
|
|
1801
|
+
const pluginReady = Boolean(pluginRoot && !pluginError);
|
|
1802
|
+
const infrastructureReady = Boolean(npmVersion && playwright?.chromiumInstalled && pluginReady);
|
|
1803
|
+
const ready = Boolean(capabilities?.ready) && infrastructureReady;
|
|
1804
|
+
return {
|
|
1805
|
+
ready,
|
|
1806
|
+
degradedReasons: ready ? [] : uniqueReasons,
|
|
1807
|
+
cliVersion: (0, clientInfo_1.getCliVersion)(),
|
|
1808
|
+
nodeVersion: node_process_1.default.versions.node,
|
|
1809
|
+
npmVersion,
|
|
1810
|
+
playwright,
|
|
1811
|
+
plugin: {
|
|
1812
|
+
ready: pluginReady,
|
|
1813
|
+
root: pluginRoot,
|
|
1814
|
+
version: pluginVersion,
|
|
1815
|
+
error: pluginError,
|
|
1816
|
+
},
|
|
1817
|
+
agents: capabilities?.agents ?? [],
|
|
1818
|
+
capabilities,
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
function getWorkerStateDir() {
|
|
1822
|
+
return node_path_1.default.join(node_os_1.default.homedir(), '.playdrop', 'worker');
|
|
1823
|
+
}
|
|
1824
|
+
function workerRuntimeStateFilePath(target) {
|
|
1825
|
+
const normalizedTarget = target === 'FIRST_PARTY' ? 'first-party' : 'personal';
|
|
1826
|
+
return node_path_1.default.join(getWorkerStateDir(), `${normalizedTarget}-runtime.json`);
|
|
1827
|
+
}
|
|
1828
|
+
function isProcessAlive(pid) {
|
|
1829
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1830
|
+
return false;
|
|
1831
|
+
}
|
|
1832
|
+
try {
|
|
1833
|
+
node_process_1.default.kill(pid, 0);
|
|
1834
|
+
return true;
|
|
1835
|
+
}
|
|
1836
|
+
catch (error) {
|
|
1837
|
+
const code = error.code;
|
|
1838
|
+
return code === 'EPERM';
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
function writeWorkerRuntimeState(state) {
|
|
1842
|
+
const file = workerRuntimeStateFilePath(state.target);
|
|
1843
|
+
(0, node_fs_1.mkdirSync)(node_path_1.default.dirname(file), { recursive: true });
|
|
1844
|
+
(0, node_fs_1.writeFileSync)(file, JSON.stringify({
|
|
1845
|
+
schemaVersion: 1,
|
|
1846
|
+
pid: node_process_1.default.pid,
|
|
1847
|
+
startedAt: new Date().toISOString(),
|
|
1848
|
+
...state,
|
|
1849
|
+
}, null, 2));
|
|
1850
|
+
}
|
|
1851
|
+
function clearWorkerRuntimeState(target) {
|
|
1852
|
+
(0, node_fs_1.rmSync)(workerRuntimeStateFilePath(target), { force: true });
|
|
1853
|
+
}
|
|
1854
|
+
function readActivePersonalWorkerRuntimeState() {
|
|
1855
|
+
const file = workerRuntimeStateFilePath('PERSONAL');
|
|
1856
|
+
let parsed;
|
|
1857
|
+
try {
|
|
1858
|
+
parsed = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
|
|
1859
|
+
}
|
|
1860
|
+
catch {
|
|
1861
|
+
return null;
|
|
1862
|
+
}
|
|
1863
|
+
if (!parsed || parsed.target !== 'PERSONAL' || !isProcessAlive(parsed.pid)) {
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
return parsed;
|
|
1867
|
+
}
|
|
1868
|
+
function getManagedWorkerPolicyFilePath(env, workerKey) {
|
|
1869
|
+
return node_path_1.default.join(getWorkerStateDir(), `update-policy-${env}-${workerKey}.json`);
|
|
1870
|
+
}
|
|
1871
|
+
function writeManagedWorkerUpdatePolicyFile(input) {
|
|
1872
|
+
if (!input.updatePolicy) {
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
const policyFile = getManagedWorkerPolicyFilePath(input.env, input.workerKey);
|
|
1876
|
+
(0, node_fs_1.mkdirSync)(node_path_1.default.dirname(policyFile), { recursive: true });
|
|
1877
|
+
(0, node_fs_1.writeFileSync)(policyFile, JSON.stringify({
|
|
1878
|
+
schemaVersion: 1,
|
|
1879
|
+
writtenAt: new Date().toISOString(),
|
|
1880
|
+
targetCliVersion: input.updatePolicy.targetCliVersion,
|
|
1881
|
+
minimumCliVersion: input.updatePolicy.minimumCliVersion,
|
|
1882
|
+
targetPluginVersion: input.updatePolicy.targetPluginVersion,
|
|
1883
|
+
minimumPluginVersion: input.updatePolicy.minimumPluginVersion,
|
|
1884
|
+
managedAgentCliVersions: input.updatePolicy.managedAgentCliVersions ?? {},
|
|
1885
|
+
requiredWrapperContractVersion: input.updatePolicy.requiredWrapperContractVersion,
|
|
1886
|
+
}, null, 2));
|
|
1887
|
+
}
|
|
1888
|
+
function shellQuote(value) {
|
|
1889
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1890
|
+
}
|
|
1891
|
+
function validatePackageVersion(value, label) {
|
|
1892
|
+
const version = typeof value === 'string' ? value.trim() : '';
|
|
1893
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
|
|
1894
|
+
throw new Error(`${label}_invalid:${version || 'missing'}`);
|
|
1895
|
+
}
|
|
1896
|
+
return version;
|
|
1897
|
+
}
|
|
1898
|
+
function verifyNpmGlobalInstallPreconditions() {
|
|
1899
|
+
const prefixProbe = (0, shellProbe_1.runShell)('npm prefix -g');
|
|
1900
|
+
const prefix = prefixProbe.output.trim().split(/\r?\n/)[0]?.trim() ?? '';
|
|
1901
|
+
if (prefixProbe.exitCode !== 0 || !prefix) {
|
|
1902
|
+
throw new Error(`worker_update_npm_prefix_unavailable: npm prefix -g exited with code ${prefixProbe.exitCode}.`);
|
|
1903
|
+
}
|
|
1904
|
+
const writeProbe = (0, shellProbe_1.runShell)(`[ -w ${shellQuote(prefix)} ]`);
|
|
1905
|
+
if (writeProbe.exitCode !== 0) {
|
|
1906
|
+
throw new Error(`worker_update_npm_prefix_not_writable: ${prefix} is not writable by ${node_os_1.default.userInfo().username}. Install Node with a user-writable global prefix before enabling managed worker updates.`);
|
|
1907
|
+
}
|
|
1908
|
+
return { prefix };
|
|
1909
|
+
}
|
|
1910
|
+
function managedWorkerWrapperPath(env) {
|
|
1911
|
+
return node_path_1.default.join(getWorkerStateDir(), `playdrop-worker-${env}.sh`);
|
|
1912
|
+
}
|
|
1913
|
+
function managedWorkerLaunchAgentLabel(env) {
|
|
1914
|
+
return `ai.playdrop.worker.${env}`;
|
|
1915
|
+
}
|
|
1916
|
+
function managedWorkerLaunchAgentPath(env) {
|
|
1917
|
+
return node_path_1.default.join(node_os_1.default.homedir(), 'Library', 'LaunchAgents', `${managedWorkerLaunchAgentLabel(env)}.plist`);
|
|
1918
|
+
}
|
|
1919
|
+
function escapeXml(value) {
|
|
1920
|
+
return value
|
|
1921
|
+
.replace(/&/g, '&')
|
|
1922
|
+
.replace(/</g, '<')
|
|
1923
|
+
.replace(/>/g, '>')
|
|
1924
|
+
.replace(/"/g, '"')
|
|
1925
|
+
.replace(/'/g, ''');
|
|
1926
|
+
}
|
|
1927
|
+
function unescapeXml(value) {
|
|
1928
|
+
return value
|
|
1929
|
+
.replace(/'/g, "'")
|
|
1930
|
+
.replace(/"/g, '"')
|
|
1931
|
+
.replace(/>/g, '>')
|
|
1932
|
+
.replace(/</g, '<')
|
|
1933
|
+
.replace(/&/g, '&');
|
|
1934
|
+
}
|
|
1935
|
+
function parseLaunchAgentProgramArguments(plist) {
|
|
1936
|
+
const arrayMatch = /<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/i.exec(plist);
|
|
1937
|
+
if (!arrayMatch?.[1]) {
|
|
1938
|
+
return [];
|
|
1939
|
+
}
|
|
1940
|
+
return Array.from(arrayMatch[1].matchAll(/<string>([\s\S]*?)<\/string>/gi))
|
|
1941
|
+
.map((match) => unescapeXml(match[1].trim()))
|
|
1942
|
+
.filter(Boolean);
|
|
1943
|
+
}
|
|
1944
|
+
function readLaunchAgentArguments(plistPath) {
|
|
1945
|
+
return parseLaunchAgentProgramArguments((0, node_fs_1.readFileSync)(plistPath, 'utf8'));
|
|
1946
|
+
}
|
|
1947
|
+
function readArgumentValue(args, flag) {
|
|
1948
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1949
|
+
const arg = args[index];
|
|
1950
|
+
if (arg === flag) {
|
|
1951
|
+
return args[index + 1] ?? null;
|
|
1952
|
+
}
|
|
1953
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
1954
|
+
return arg.slice(flag.length + 1);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
return null;
|
|
1958
|
+
}
|
|
1959
|
+
function isPlaydropWorkerStartCommand(args) {
|
|
1960
|
+
const joined = args.join(' ');
|
|
1961
|
+
return /\bplaydrop\b/.test(joined) && args.includes('worker') && args.includes('start');
|
|
1962
|
+
}
|
|
1963
|
+
function isCompatibleLegacyWorkerLaunchAgent(input) {
|
|
1964
|
+
if (!isPlaydropWorkerStartCommand(input.args)) {
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
return readArgumentValue(input.args, '--env') === input.env
|
|
1968
|
+
&& readArgumentValue(input.args, '--name') === input.workerName;
|
|
1969
|
+
}
|
|
1970
|
+
function assertLaunchAgentArgumentsCompatible(input) {
|
|
1971
|
+
const normalizedPlistPath = node_path_1.default.resolve(input.plistPath);
|
|
1972
|
+
const normalizedCanonicalPath = node_path_1.default.resolve(input.canonicalPlistPath);
|
|
1973
|
+
if (normalizedPlistPath === normalizedCanonicalPath) {
|
|
1974
|
+
if (input.args.length === 1 && input.args[0] === input.wrapperPath) {
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
throw new Error(`worker_supervisor_conflict: ${input.plistPath} already exists but does not point at the managed PlayDrop worker wrapper ${input.wrapperPath}. Refusing to overwrite it.`);
|
|
1978
|
+
}
|
|
1979
|
+
if (!isPlaydropWorkerStartCommand(input.args) && input.args[0] !== input.wrapperPath) {
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
if (input.args[0] === input.wrapperPath
|
|
1983
|
+
|| isCompatibleLegacyWorkerLaunchAgent({
|
|
1984
|
+
args: input.args,
|
|
1985
|
+
env: input.env,
|
|
1986
|
+
workerName: input.workerName,
|
|
1987
|
+
})) {
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
throw new Error(`worker_supervisor_conflict: ${input.plistPath} appears to manage a different PlayDrop worker. Refusing to install a second supervisor.`);
|
|
1991
|
+
}
|
|
1992
|
+
function assertLaunchAgentPlistCompatible(input) {
|
|
1993
|
+
assertLaunchAgentArgumentsCompatible({
|
|
1994
|
+
plistPath: input.plistPath,
|
|
1995
|
+
args: parseLaunchAgentProgramArguments(input.plist),
|
|
1996
|
+
env: input.env,
|
|
1997
|
+
workerName: input.workerName,
|
|
1998
|
+
wrapperPath: input.wrapperPath,
|
|
1999
|
+
canonicalPlistPath: input.canonicalPlistPath,
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
function assertNoSupervisorConflicts(input) {
|
|
2003
|
+
const launchAgentsDir = node_path_1.default.dirname(input.canonicalPlistPath);
|
|
2004
|
+
if ((0, node_fs_1.existsSync)(input.canonicalPlistPath)) {
|
|
2005
|
+
assertLaunchAgentArgumentsCompatible({
|
|
2006
|
+
...input,
|
|
2007
|
+
plistPath: input.canonicalPlistPath,
|
|
2008
|
+
args: readLaunchAgentArguments(input.canonicalPlistPath),
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
if (!(0, node_fs_1.existsSync)(launchAgentsDir)) {
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
for (const entry of (0, node_fs_1.readdirSync)(launchAgentsDir)) {
|
|
2015
|
+
if (!entry.endsWith('.plist')) {
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
2018
|
+
const plistPath = node_path_1.default.join(launchAgentsDir, entry);
|
|
2019
|
+
if (node_path_1.default.resolve(plistPath) === node_path_1.default.resolve(input.canonicalPlistPath)) {
|
|
2020
|
+
continue;
|
|
2021
|
+
}
|
|
2022
|
+
let args;
|
|
2023
|
+
try {
|
|
2024
|
+
args = readLaunchAgentArguments(plistPath);
|
|
2025
|
+
}
|
|
2026
|
+
catch {
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
assertLaunchAgentArgumentsCompatible({
|
|
2030
|
+
...input,
|
|
2031
|
+
plistPath,
|
|
2032
|
+
args,
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
async function writeManagedWorkerWrapper(input) {
|
|
2037
|
+
const wrapperPath = managedWorkerWrapperPath(input.env);
|
|
2038
|
+
const policyFile = getManagedWorkerPolicyFilePath(input.env, input.workerKey);
|
|
2039
|
+
const failureFile = node_path_1.default.join(getWorkerStateDir(), `update-failure-${input.env}-${input.workerKey}.json`);
|
|
2040
|
+
await (0, promises_1.mkdir)(node_path_1.default.dirname(wrapperPath), { recursive: true });
|
|
2041
|
+
const script = `#!/bin/sh
|
|
2042
|
+
set -eu
|
|
2043
|
+
|
|
2044
|
+
export PLAYDROP_WORKER_WRAPPER_CONTRACT_VERSION="${WORKER_WRAPPER_CONTRACT_VERSION}"
|
|
2045
|
+
POLICY_FILE="${policyFile}"
|
|
2046
|
+
FAILURE_FILE="${failureFile}"
|
|
2047
|
+
BACKOFF_SECONDS="\${PLAYDROP_WORKER_UPDATE_FAILURE_BACKOFF_SECONDS:-30}"
|
|
2048
|
+
|
|
2049
|
+
record_failure() {
|
|
2050
|
+
node - "$FAILURE_FILE" "$1" <<'NODE'
|
|
2051
|
+
const fs = require("fs");
|
|
2052
|
+
const path = require("path");
|
|
2053
|
+
const file = process.argv[2];
|
|
2054
|
+
const error = process.argv[3] || "worker_update_install_failed";
|
|
2055
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
2056
|
+
fs.writeFileSync(file, JSON.stringify({
|
|
2057
|
+
schemaVersion: 1,
|
|
2058
|
+
writtenAt: new Date().toISOString(),
|
|
2059
|
+
error,
|
|
2060
|
+
}, null, 2));
|
|
2061
|
+
NODE
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
fail_converge() {
|
|
2065
|
+
code="$1"
|
|
2066
|
+
reason="$2"
|
|
2067
|
+
record_failure "$reason"
|
|
2068
|
+
sleep "$BACKOFF_SECONDS"
|
|
2069
|
+
exit "$code"
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
run_or_fail() {
|
|
2073
|
+
reason="$1"
|
|
2074
|
+
shift
|
|
2075
|
+
"$@" || fail_converge "$?" "$reason"
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
validate_policy_file() {
|
|
2079
|
+
if [ ! -f "$POLICY_FILE" ]; then
|
|
2080
|
+
return 0
|
|
2081
|
+
fi
|
|
2082
|
+
node - "$POLICY_FILE" <<'NODE' || fail_converge "$?" "worker_update_policy_unavailable"
|
|
2083
|
+
const fs = require("fs");
|
|
2084
|
+
const file = process.argv[2];
|
|
2085
|
+
try {
|
|
2086
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
2087
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2088
|
+
process.exit(1);
|
|
2089
|
+
}
|
|
2090
|
+
} catch {
|
|
2091
|
+
process.exit(1);
|
|
2092
|
+
}
|
|
2093
|
+
NODE
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
read_policy_value() {
|
|
2097
|
+
node -e 'const fs = require("fs"); const file = process.argv[1]; const key = process.argv[2]; if (!fs.existsSync(file)) process.exit(0); const policy = JSON.parse(fs.readFileSync(file, "utf8")); const value = policy && policy[key]; if (typeof value === "string") process.stdout.write(value);' "$POLICY_FILE" "$1"
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
validate_policy_file
|
|
2101
|
+
TARGET_CLI_VERSION="$(read_policy_value targetCliVersion)"
|
|
2102
|
+
TARGET_PLUGIN_VERSION="$(read_policy_value targetPluginVersion)"
|
|
2103
|
+
|
|
2104
|
+
if [ -n "$TARGET_CLI_VERSION" ]; then
|
|
2105
|
+
run_or_fail "worker_update_cli_install_failed" npm install -g --no-audit --no-fund "@playdrop/playdrop-cli@$TARGET_CLI_VERSION"
|
|
2106
|
+
fi
|
|
2107
|
+
|
|
2108
|
+
if [ -n "$TARGET_PLUGIN_VERSION" ]; then
|
|
2109
|
+
if command -v codex >/dev/null 2>&1; then
|
|
2110
|
+
run_or_fail "worker_update_plugin_install_failed:codex" playdrop agents update-plugin codex
|
|
2111
|
+
fi
|
|
2112
|
+
if command -v claude >/dev/null 2>&1; then
|
|
2113
|
+
run_or_fail "worker_update_plugin_install_failed:claude" playdrop agents update-plugin claude
|
|
2114
|
+
fi
|
|
2115
|
+
node - "$TARGET_PLUGIN_VERSION" <<'NODE' || fail_converge "$?" "worker_update_plugin_version_mismatch"
|
|
2116
|
+
const childProcess = require("child_process");
|
|
2117
|
+
const targetVersion = process.argv[2];
|
|
2118
|
+
const result = childProcess.spawnSync("playdrop", ["agents", "status", "--json"], { encoding: "utf8" });
|
|
2119
|
+
if (result.status !== 0) {
|
|
2120
|
+
process.exit(1);
|
|
2121
|
+
}
|
|
2122
|
+
let parsed;
|
|
2123
|
+
try {
|
|
2124
|
+
parsed = JSON.parse(result.stdout);
|
|
2125
|
+
} catch {
|
|
2126
|
+
process.exit(1);
|
|
2127
|
+
}
|
|
2128
|
+
const agents = Array.isArray(parsed && parsed.agents) ? parsed.agents : [];
|
|
2129
|
+
const checked = agents.filter((agent) => agent && agent.id !== "cursor" && agent.cli && agent.cli.installed);
|
|
2130
|
+
for (const agent of checked) {
|
|
2131
|
+
const version = agent && agent.plugin && typeof agent.plugin.version === "string" ? agent.plugin.version.trim() : "";
|
|
2132
|
+
if (version !== targetVersion) {
|
|
2133
|
+
process.exit(1);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
NODE
|
|
2137
|
+
fi
|
|
2138
|
+
|
|
2139
|
+
node - "$POLICY_FILE" <<'NODE' || fail_converge "$?" "worker_update_agent_cli_install_failed"
|
|
2140
|
+
const childProcess = require("child_process");
|
|
2141
|
+
const fs = require("fs");
|
|
2142
|
+
|
|
2143
|
+
const policyFile = process.argv[2];
|
|
2144
|
+
if (!policyFile || !fs.existsSync(policyFile)) {
|
|
2145
|
+
process.exit(0);
|
|
2146
|
+
}
|
|
2147
|
+
const policy = JSON.parse(fs.readFileSync(policyFile, "utf8"));
|
|
2148
|
+
const versions = policy && policy.managedAgentCliVersions && typeof policy.managedAgentCliVersions === "object"
|
|
2149
|
+
? policy.managedAgentCliVersions
|
|
2150
|
+
: {};
|
|
2151
|
+
const packages = {
|
|
2152
|
+
CODEX: "@openai/codex",
|
|
2153
|
+
CLAUDE_CODE: "@anthropic-ai/claude-code",
|
|
2154
|
+
};
|
|
2155
|
+
const aliases = {
|
|
2156
|
+
CODEX: "CODEX",
|
|
2157
|
+
OPENAI_CODEX: "CODEX",
|
|
2158
|
+
CLAUDE: "CLAUDE_CODE",
|
|
2159
|
+
CLAUDE_CODE: "CLAUDE_CODE",
|
|
2160
|
+
CURSOR: "CURSOR_COMPOSER",
|
|
2161
|
+
CURSOR_COMPOSER: "CURSOR_COMPOSER",
|
|
2162
|
+
};
|
|
2163
|
+
for (const [rawKey, rawVersion] of Object.entries(versions)) {
|
|
2164
|
+
const version = typeof rawVersion === "string" ? rawVersion.trim() : "";
|
|
2165
|
+
if (!/^\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
|
|
2166
|
+
throw new Error(\`worker_update_agent_cli_version_invalid:\${rawKey}\`);
|
|
2167
|
+
}
|
|
2168
|
+
const normalizedKey = aliases[String(rawKey).trim().toUpperCase().replace(/[-\\s]+/g, "_")];
|
|
2169
|
+
if (normalizedKey === "CURSOR_COMPOSER") {
|
|
2170
|
+
throw new Error("worker_update_cursor_cli_auto_update_unsupported");
|
|
2171
|
+
}
|
|
2172
|
+
const packageName = normalizedKey ? packages[normalizedKey] : null;
|
|
2173
|
+
if (!packageName) {
|
|
2174
|
+
throw new Error(\`worker_update_agent_cli_unknown:\${rawKey}\`);
|
|
2175
|
+
}
|
|
2176
|
+
const result = childProcess.spawnSync(
|
|
2177
|
+
"npm",
|
|
2178
|
+
["install", "-g", "--no-audit", "--no-fund", \`\${packageName}@\${version}\`],
|
|
2179
|
+
{ stdio: "inherit" },
|
|
2180
|
+
);
|
|
2181
|
+
if (result.status !== 0) {
|
|
2182
|
+
throw new Error(\`worker_update_agent_cli_install_failed:\${normalizedKey}\`);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
NODE
|
|
2186
|
+
|
|
2187
|
+
exec playdrop worker start --env ${shellQuote(input.env)} --name ${shellQuote(input.workerName)}
|
|
2188
|
+
`;
|
|
2189
|
+
await (0, promises_1.writeFile)(wrapperPath, script, 'utf8');
|
|
2190
|
+
await (0, promises_1.chmod)(wrapperPath, 0o755);
|
|
2191
|
+
return { wrapperPath, policyFile };
|
|
2192
|
+
}
|
|
2193
|
+
async function writeManagedWorkerLaunchAgent(input) {
|
|
2194
|
+
if (node_process_1.default.platform !== 'darwin') {
|
|
2195
|
+
throw new Error('worker_setup_launchagent_unsupported: managed worker setup currently supports macOS LaunchAgent only.');
|
|
2196
|
+
}
|
|
2197
|
+
const label = managedWorkerLaunchAgentLabel(input.env);
|
|
2198
|
+
const plistPath = managedWorkerLaunchAgentPath(input.env);
|
|
2199
|
+
const logDir = node_path_1.default.join(getWorkerStateDir(), 'logs');
|
|
2200
|
+
assertNoSupervisorConflicts({
|
|
2201
|
+
env: input.env,
|
|
2202
|
+
workerName: input.workerName,
|
|
2203
|
+
wrapperPath: input.wrapperPath,
|
|
2204
|
+
canonicalPlistPath: plistPath,
|
|
2205
|
+
});
|
|
2206
|
+
await (0, promises_1.mkdir)(node_path_1.default.dirname(plistPath), { recursive: true });
|
|
2207
|
+
await (0, promises_1.mkdir)(logDir, { recursive: true });
|
|
2208
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2209
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2210
|
+
<plist version="1.0">
|
|
2211
|
+
<dict>
|
|
2212
|
+
<key>Label</key>
|
|
2213
|
+
<string>${escapeXml(label)}</string>
|
|
2214
|
+
<key>ProgramArguments</key>
|
|
2215
|
+
<array>
|
|
2216
|
+
<string>${escapeXml(input.wrapperPath)}</string>
|
|
2217
|
+
</array>
|
|
2218
|
+
<key>RunAtLoad</key>
|
|
2219
|
+
<true/>
|
|
2220
|
+
<key>KeepAlive</key>
|
|
2221
|
+
<true/>
|
|
2222
|
+
<key>EnvironmentVariables</key>
|
|
2223
|
+
<dict>
|
|
2224
|
+
<key>PLAYDROP_WORKER_WRAPPER_CONTRACT_VERSION</key>
|
|
2225
|
+
<string>${WORKER_WRAPPER_CONTRACT_VERSION}</string>
|
|
2226
|
+
</dict>
|
|
2227
|
+
<key>StandardOutPath</key>
|
|
2228
|
+
<string>${escapeXml(node_path_1.default.join(logDir, `${label}.out.log`))}</string>
|
|
2229
|
+
<key>StandardErrorPath</key>
|
|
2230
|
+
<string>${escapeXml(node_path_1.default.join(logDir, `${label}.err.log`))}</string>
|
|
2231
|
+
</dict>
|
|
2232
|
+
</plist>
|
|
2233
|
+
`;
|
|
2234
|
+
await (0, promises_1.writeFile)(plistPath, plist, 'utf8');
|
|
2235
|
+
return { label, plistPath };
|
|
2236
|
+
}
|
|
2237
|
+
function launchManagedWorker(label, plistPath) {
|
|
2238
|
+
const uid = typeof node_process_1.default.getuid === 'function' ? node_process_1.default.getuid() : null;
|
|
2239
|
+
if (uid === null) {
|
|
2240
|
+
throw new Error('worker_setup_launchagent_uid_unavailable');
|
|
2241
|
+
}
|
|
2242
|
+
const domain = `gui/${uid}`;
|
|
2243
|
+
const bootstrap = (0, shellProbe_1.runShell)(`launchctl bootstrap ${shellQuote(domain)} ${shellQuote(plistPath)}`);
|
|
2244
|
+
if (bootstrap.exitCode !== 0 && !/already|exists|service name is already registered/i.test(bootstrap.output)) {
|
|
2245
|
+
throw new Error(`worker_setup_launchagent_start_failed: launchctl bootstrap exited with code ${bootstrap.exitCode}. ${bootstrap.output.slice(0, 300)}`);
|
|
2246
|
+
}
|
|
2247
|
+
const kickstart = (0, shellProbe_1.runShell)(`launchctl kickstart -k ${shellQuote(`${domain}/${label}`)}`);
|
|
2248
|
+
if (kickstart.exitCode !== 0) {
|
|
2249
|
+
throw new Error(`worker_setup_launchagent_start_failed: launchctl kickstart exited with code ${kickstart.exitCode}. ${kickstart.output.slice(0, 300)}`);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
async function runPluginSetupAction(action, agent) {
|
|
2253
|
+
const previousExitCode = node_process_1.default.exitCode;
|
|
2254
|
+
node_process_1.default.exitCode = undefined;
|
|
2255
|
+
try {
|
|
2256
|
+
if (action === 'install') {
|
|
2257
|
+
await (0, agents_1.installPlugin)(agent);
|
|
2258
|
+
}
|
|
2259
|
+
else {
|
|
2260
|
+
await (0, agents_1.updatePlugin)(agent);
|
|
2261
|
+
}
|
|
2262
|
+
if (node_process_1.default.exitCode && node_process_1.default.exitCode !== 0) {
|
|
2263
|
+
throw new Error(`worker_setup_plugin_${action}_failed:${agent}`);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
finally {
|
|
2267
|
+
node_process_1.default.exitCode = previousExitCode;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
async function ensurePlaydropPluginsForSetup(local) {
|
|
2271
|
+
for (const agent of local.agents) {
|
|
2272
|
+
if (agent.agent === 'CODEX') {
|
|
2273
|
+
await runPluginSetupAction(local.plugin.ready ? 'update' : 'install', 'codex');
|
|
2274
|
+
}
|
|
2275
|
+
else if (agent.agent === 'CLAUDE_CODE') {
|
|
2276
|
+
await runPluginSetupAction(local.plugin.ready ? 'update' : 'install', 'claude');
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
async function resolveWorkerStatusPayload(options = {}) {
|
|
2281
|
+
loadWorkerEnvFile(options.env);
|
|
2282
|
+
const local = collectWorkerLocalStatus();
|
|
2283
|
+
const server = {
|
|
2284
|
+
reachable: false,
|
|
2285
|
+
authenticated: false,
|
|
2286
|
+
worker: null,
|
|
2287
|
+
};
|
|
2288
|
+
let ctx;
|
|
2289
|
+
try {
|
|
2290
|
+
ctx = await (0, commandContext_1.resolveOptionalEnvironmentContext)('worker status', { env: options.env });
|
|
2291
|
+
}
|
|
2292
|
+
catch (error) {
|
|
2293
|
+
return {
|
|
2294
|
+
schemaVersion: 1,
|
|
2295
|
+
generatedAt: new Date().toISOString(),
|
|
2296
|
+
local,
|
|
2297
|
+
server: {
|
|
2298
|
+
...server,
|
|
2299
|
+
error: errorCode(error),
|
|
2300
|
+
},
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
if (!ctx?.token) {
|
|
2304
|
+
return {
|
|
2305
|
+
schemaVersion: 1,
|
|
2306
|
+
generatedAt: new Date().toISOString(),
|
|
2307
|
+
local,
|
|
2308
|
+
server: {
|
|
2309
|
+
...server,
|
|
2310
|
+
error: 'not_authenticated',
|
|
2311
|
+
},
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
server.authenticated = true;
|
|
2315
|
+
const workerKey = (0, config_1.loadConfig)().workerKey?.trim() ?? '';
|
|
2316
|
+
try {
|
|
2317
|
+
const workers = await ctx.client.listMyAgentWorkers();
|
|
2318
|
+
server.reachable = true;
|
|
2319
|
+
server.worker = workerKey
|
|
2320
|
+
? workers.workers.find((worker) => worker.workerKey === workerKey) ?? null
|
|
2321
|
+
: null;
|
|
2322
|
+
}
|
|
2323
|
+
catch (error) {
|
|
2324
|
+
server.error = errorCode(error);
|
|
2325
|
+
}
|
|
2326
|
+
return {
|
|
2327
|
+
schemaVersion: 1,
|
|
2328
|
+
generatedAt: new Date().toISOString(),
|
|
2329
|
+
local,
|
|
2330
|
+
server,
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
function printWorkerStatus(payload) {
|
|
2334
|
+
console.log(`Local worker: ${payload.local.ready ? 'ready' : 'degraded'}`);
|
|
2335
|
+
console.log(` CLI: ${payload.local.cliVersion}`);
|
|
2336
|
+
console.log(` Node: ${payload.local.nodeVersion}`);
|
|
2337
|
+
console.log(` npm: ${payload.local.npmVersion ?? 'missing'}`);
|
|
2338
|
+
console.log(` PlayDrop plugin: ${payload.local.plugin.ready ? payload.local.plugin.version ?? payload.local.plugin.root ?? 'installed' : 'missing'}`);
|
|
2339
|
+
if (payload.local.agents.length > 0) {
|
|
2340
|
+
for (const agent of payload.local.agents) {
|
|
2341
|
+
console.log(` ${agent.agent}: ${agent.ready ? 'ready' : 'degraded'} ${agent.cliVersion ?? ''}`.trimEnd());
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
else {
|
|
2345
|
+
console.log(' Agents: none detected');
|
|
2346
|
+
}
|
|
2347
|
+
if (payload.local.degradedReasons.length > 0) {
|
|
2348
|
+
console.log(` Reasons: ${payload.local.degradedReasons.join(', ')}`);
|
|
2349
|
+
}
|
|
2350
|
+
if (!payload.server.authenticated) {
|
|
2351
|
+
console.log('Server worker: not authenticated');
|
|
2352
|
+
}
|
|
2353
|
+
else if (!payload.server.reachable) {
|
|
2354
|
+
console.log(`Server worker: unreachable${payload.server.error ? ` (${payload.server.error})` : ''}`);
|
|
2355
|
+
}
|
|
2356
|
+
else if (!payload.server.worker) {
|
|
2357
|
+
console.log('Server worker: none registered');
|
|
2358
|
+
}
|
|
2359
|
+
else {
|
|
2360
|
+
console.log(`Server worker: ${payload.server.worker.name} (${payload.server.worker.lifecycleState ?? 'unknown'}, ${payload.server.worker.updateState ?? 'unknown'})`);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
async function showWorkerStatus(options = {}) {
|
|
2364
|
+
const payload = await resolveWorkerStatusPayload(options);
|
|
2365
|
+
if (options.json) {
|
|
2366
|
+
(0, output_1.printJson)(payload);
|
|
2367
|
+
}
|
|
2368
|
+
else {
|
|
2369
|
+
printWorkerStatus(payload);
|
|
2370
|
+
}
|
|
2371
|
+
return payload;
|
|
2372
|
+
}
|
|
2373
|
+
async function setupWorker(options = {}) {
|
|
2374
|
+
loadWorkerEnvFile(options.env);
|
|
2375
|
+
await (0, commandContext_1.withEnvironment)('worker setup', 'Setting up the PlayDrop worker', async ({ client, env }) => {
|
|
2376
|
+
const me = await client.me();
|
|
2377
|
+
const username = me.user?.username?.trim();
|
|
2378
|
+
if (!username) {
|
|
2379
|
+
throw new Error('worker_session_invalid: the stored session did not resolve to a user. Run "playdrop auth login" and retry.');
|
|
2380
|
+
}
|
|
2381
|
+
const target = resolveWorkerExecutionTargetFromRole(me.user.role);
|
|
2382
|
+
const workerKey = (0, config_1.getOrCreateWorkerKey)();
|
|
2383
|
+
const explicitWorkerName = options.name?.trim() || '';
|
|
2384
|
+
const workerName = explicitWorkerName || `${username} - ${node_os_1.default.hostname()}`;
|
|
2385
|
+
const local = collectWorkerLocalStatus();
|
|
2386
|
+
const agentReady = local.agents.some((agent) => agent.ready === true);
|
|
2387
|
+
const baseRuntimeReady = Boolean(agentReady && local.npmVersion && local.playwright?.chromiumInstalled);
|
|
2388
|
+
if (!baseRuntimeReady) {
|
|
2389
|
+
throw new Error(`worker_setup_preflight_failed: ${local.degradedReasons.join(', ') || 'local worker runtime is not ready'}`);
|
|
2390
|
+
}
|
|
2391
|
+
await ensurePlaydropPluginsForSetup(local);
|
|
2392
|
+
const postPluginLocal = collectWorkerLocalStatus();
|
|
2393
|
+
if (!postPluginLocal.plugin.ready) {
|
|
2394
|
+
throw new Error(`worker_setup_plugin_unavailable: ${postPluginLocal.plugin.error ?? 'PlayDrop plugin could not be verified after setup'}`);
|
|
2395
|
+
}
|
|
2396
|
+
if (target !== 'FIRST_PARTY') {
|
|
2397
|
+
(0, output_1.printSuccess)('Personal worker setup complete.', [
|
|
2398
|
+
'Run: playdrop worker start --supervise-stdin',
|
|
2399
|
+
]);
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
if (!options.env?.trim()) {
|
|
2403
|
+
throw new Error('worker_setup_requires_env: admin worker setup must pass --env <env> so the LaunchAgent cannot be installed for the wrong environment.');
|
|
2404
|
+
}
|
|
2405
|
+
if (!explicitWorkerName) {
|
|
2406
|
+
throw new Error('worker_setup_requires_name: admin worker setup must pass --name <worker name> so the LaunchAgent identity is deterministic.');
|
|
2407
|
+
}
|
|
2408
|
+
verifyNpmGlobalInstallPreconditions();
|
|
2409
|
+
const { wrapperPath, policyFile } = await writeManagedWorkerWrapper({ env, workerName, workerKey });
|
|
2410
|
+
const { label, plistPath } = await writeManagedWorkerLaunchAgent({ env, workerName, wrapperPath });
|
|
2411
|
+
const status = await resolveWorkerStatusPayload({ env });
|
|
2412
|
+
const activeTaskCount = status.server.worker?.activeTaskCount ?? 0;
|
|
2413
|
+
const shouldStart = options.start !== false;
|
|
2414
|
+
if (activeTaskCount > 0 && shouldStart) {
|
|
2415
|
+
await client.workerCreateUpdateIntent({
|
|
2416
|
+
workerKey,
|
|
2417
|
+
environment: env,
|
|
2418
|
+
reason: 'setup_apply',
|
|
2419
|
+
});
|
|
2420
|
+
(0, output_1.printSuccess)('Managed worker setup updated and queued for quiesced restart.', [
|
|
2421
|
+
`Wrapper: ${wrapperPath}`,
|
|
2422
|
+
`LaunchAgent: ${plistPath}`,
|
|
2423
|
+
`Policy file: ${policyFile}`,
|
|
2424
|
+
]);
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
if (shouldStart || options.restart) {
|
|
2428
|
+
launchManagedWorker(label, plistPath);
|
|
2429
|
+
}
|
|
2430
|
+
(0, output_1.printSuccess)('Managed worker setup complete.', [
|
|
2431
|
+
`Wrapper: ${wrapperPath}`,
|
|
2432
|
+
`LaunchAgent: ${plistPath}`,
|
|
2433
|
+
`Policy file: ${policyFile}`,
|
|
2434
|
+
]);
|
|
2435
|
+
}, { env: options.env });
|
|
2436
|
+
}
|
|
1662
2437
|
async function failTaskWithRetry(client, taskId, body) {
|
|
1663
2438
|
try {
|
|
1664
2439
|
await client.workerFailAgentTask(taskId, body);
|
|
@@ -1708,11 +2483,14 @@ async function startWorker(options = {}) {
|
|
|
1708
2483
|
const target = resolveWorkerExecutionTargetFromRole(me.user.role);
|
|
1709
2484
|
const codexVersion = probeCodexInstallation()?.codexVersion ?? null;
|
|
1710
2485
|
const claudeVersion = probeClaudeInstallation()?.claudeVersion ?? null;
|
|
2486
|
+
const cursorVersion = probeCursorInstallation()?.cursorVersion ?? null;
|
|
1711
2487
|
const codexAuthenticated = codexVersion ? readCodexAuthenticated() : false;
|
|
1712
2488
|
const claudeAuthenticated = claudeVersion ? readClaudeAuthenticated() : false;
|
|
2489
|
+
const cursorAuthenticated = cursorVersion ? readCursorAuthenticated() : false;
|
|
1713
2490
|
const npmVersion = probeNpmVersion();
|
|
1714
2491
|
const playwright = probePlaywrightChromium();
|
|
1715
2492
|
const playdropPluginRoot = resolvePlaydropPluginRoot();
|
|
2493
|
+
const playdropPluginVersion = readPlaydropPluginVersion(playdropPluginRoot);
|
|
1716
2494
|
const workerKey = (0, config_1.getOrCreateWorkerKey)();
|
|
1717
2495
|
const workerName = options.name?.trim() || `${username} - ${node_os_1.default.hostname()}`;
|
|
1718
2496
|
const maxParallelTasks = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_MAX_PARALLEL_TASKS', DEFAULT_WORKER_MAX_PARALLEL_TASKS);
|
|
@@ -1721,17 +2499,31 @@ async function startWorker(options = {}) {
|
|
|
1721
2499
|
codexAuthenticated,
|
|
1722
2500
|
claudeVersion,
|
|
1723
2501
|
claudeAuthenticated,
|
|
2502
|
+
cursorVersion,
|
|
2503
|
+
cursorAuthenticated,
|
|
1724
2504
|
npmVersion,
|
|
1725
2505
|
playwright,
|
|
1726
2506
|
maxParallelTasks,
|
|
1727
2507
|
runningTaskCount: 0,
|
|
1728
2508
|
});
|
|
2509
|
+
if (playdropPluginVersion) {
|
|
2510
|
+
capabilities.pluginVersion = playdropPluginVersion;
|
|
2511
|
+
for (const agent of capabilities.agents) {
|
|
2512
|
+
agent.pluginVersion = playdropPluginVersion;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
1729
2515
|
if (!capabilities.ready) {
|
|
1730
2516
|
const reasons = Array.isArray(capabilities.degradedReasons) && capabilities.degradedReasons.length > 0
|
|
1731
2517
|
? capabilities.degradedReasons.join(', ')
|
|
1732
2518
|
: 'no ready agent';
|
|
1733
2519
|
throw new Error(`worker_preflight_failed: ${reasons}`);
|
|
1734
2520
|
}
|
|
2521
|
+
writeWorkerRuntimeState({
|
|
2522
|
+
env,
|
|
2523
|
+
workerKey,
|
|
2524
|
+
workerName,
|
|
2525
|
+
target,
|
|
2526
|
+
});
|
|
1735
2527
|
const activeTaskIds = new Set();
|
|
1736
2528
|
const activeTaskRuns = new Map();
|
|
1737
2529
|
const activeTerminators = new Map();
|
|
@@ -1739,6 +2531,7 @@ async function startWorker(options = {}) {
|
|
|
1739
2531
|
const workerDevPortBase = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_DEV_PORT_BASE', DEFAULT_WORKER_DEV_PORT_BASE);
|
|
1740
2532
|
let sessionExpired = false;
|
|
1741
2533
|
let shuttingDown = false;
|
|
2534
|
+
let quiescingForUpdate = false;
|
|
1742
2535
|
let shutdownSignal = null;
|
|
1743
2536
|
let crashed = false;
|
|
1744
2537
|
let fatalTaskError = null;
|
|
@@ -1758,6 +2551,12 @@ async function startWorker(options = {}) {
|
|
|
1758
2551
|
name: workerName,
|
|
1759
2552
|
environment: env,
|
|
1760
2553
|
capabilities: currentCapabilities(),
|
|
2554
|
+
lifecycleState: quiescingForUpdate ? 'QUIESCING' : 'READY',
|
|
2555
|
+
updateState: quiescingForUpdate ? 'UPDATE_REQUIRED' : 'CURRENT',
|
|
2556
|
+
ready: capabilities.ready,
|
|
2557
|
+
activeTaskCount: activeTaskRuns.size,
|
|
2558
|
+
cliVersion: (0, clientInfo_1.getCliVersion)(),
|
|
2559
|
+
pluginVersion: capabilities.pluginVersion,
|
|
1761
2560
|
});
|
|
1762
2561
|
const recordFatalTaskError = (error) => {
|
|
1763
2562
|
if (!fatalTaskError) {
|
|
@@ -1766,6 +2565,24 @@ async function startWorker(options = {}) {
|
|
|
1766
2565
|
shuttingDown = true;
|
|
1767
2566
|
terminateActiveTasks();
|
|
1768
2567
|
};
|
|
2568
|
+
const handleWorkerAction = (response) => {
|
|
2569
|
+
const requiredWrapperVersion = response?.updatePolicy?.requiredWrapperContractVersion;
|
|
2570
|
+
if (typeof requiredWrapperVersion === 'number'
|
|
2571
|
+
&& requiredWrapperVersion > WORKER_WRAPPER_CONTRACT_VERSION) {
|
|
2572
|
+
recordFatalTaskError(new Error(`worker_wrapper_update_required: run "playdrop worker setup --env ${env}" to install wrapper contract ${requiredWrapperVersion}.`));
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
if (response?.action !== 'quiesce_for_update' || quiescingForUpdate) {
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
writeManagedWorkerUpdatePolicyFile({
|
|
2579
|
+
env,
|
|
2580
|
+
workerKey,
|
|
2581
|
+
updatePolicy: response.updatePolicy,
|
|
2582
|
+
});
|
|
2583
|
+
quiescingForUpdate = true;
|
|
2584
|
+
console.log('Worker entering QUIESCING for managed update. No new tasks will be claimed.');
|
|
2585
|
+
};
|
|
1769
2586
|
const signalHandler = (signal) => {
|
|
1770
2587
|
shutdownSignal = signal;
|
|
1771
2588
|
if (shuttingDown) {
|
|
@@ -1775,8 +2592,21 @@ async function startWorker(options = {}) {
|
|
|
1775
2592
|
// Task runs finish their own lifecycle (flush, fail, clean) before exit.
|
|
1776
2593
|
terminateActiveTasks();
|
|
1777
2594
|
};
|
|
2595
|
+
const stdinShutdownHandler = () => {
|
|
2596
|
+
shutdownSignal = 'SIGTERM';
|
|
2597
|
+
if (shuttingDown) {
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
shuttingDown = true;
|
|
2601
|
+
terminateActiveTasks();
|
|
2602
|
+
};
|
|
1778
2603
|
node_process_1.default.once('SIGINT', signalHandler);
|
|
1779
2604
|
node_process_1.default.once('SIGTERM', signalHandler);
|
|
2605
|
+
if (options.superviseStdin) {
|
|
2606
|
+
node_process_1.default.stdin.once('end', stdinShutdownHandler);
|
|
2607
|
+
node_process_1.default.stdin.once('close', stdinShutdownHandler);
|
|
2608
|
+
node_process_1.default.stdin.resume();
|
|
2609
|
+
}
|
|
1780
2610
|
console.log(`PlayDrop Worker started as "${workerName}" (${target}).`);
|
|
1781
2611
|
await sendWorkerHealthAlertSafely({ state: 'started', env, workerName });
|
|
1782
2612
|
// Marks the session expired, kills any in-flight agent child, and lets the
|
|
@@ -1790,7 +2620,7 @@ async function startWorker(options = {}) {
|
|
|
1790
2620
|
terminateActiveTasks();
|
|
1791
2621
|
};
|
|
1792
2622
|
try {
|
|
1793
|
-
await client.workerPresence(presenceBody());
|
|
2623
|
+
handleWorkerAction(await client.workerPresence(presenceBody()));
|
|
1794
2624
|
}
|
|
1795
2625
|
catch (error) {
|
|
1796
2626
|
if (isWorkerAuthFailureError(error)) {
|
|
@@ -1799,7 +2629,7 @@ async function startWorker(options = {}) {
|
|
|
1799
2629
|
throw error;
|
|
1800
2630
|
}
|
|
1801
2631
|
presenceTimer = setInterval(() => {
|
|
1802
|
-
client.workerPresence(presenceBody()).catch((error) => {
|
|
2632
|
+
client.workerPresence(presenceBody()).then(handleWorkerAction).catch((error) => {
|
|
1803
2633
|
if (isWorkerAuthFailureError(error)) {
|
|
1804
2634
|
handleSessionExpiry();
|
|
1805
2635
|
return;
|
|
@@ -2009,8 +2839,8 @@ async function startWorker(options = {}) {
|
|
|
2009
2839
|
throw new Error('worker_shutdown');
|
|
2010
2840
|
}
|
|
2011
2841
|
agentStartedAt = new Date();
|
|
2012
|
-
|
|
2013
|
-
|
|
2842
|
+
if (assignment.agent === 'CODEX') {
|
|
2843
|
+
agentResult = await runCodex({
|
|
2014
2844
|
workspaceDir,
|
|
2015
2845
|
binDir,
|
|
2016
2846
|
eventDir,
|
|
@@ -2026,8 +2856,10 @@ async function startWorker(options = {}) {
|
|
|
2026
2856
|
onChild: (controls) => {
|
|
2027
2857
|
activeTerminators.set(task.id, controls.terminate);
|
|
2028
2858
|
},
|
|
2029
|
-
})
|
|
2030
|
-
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
else if (assignment.agent === 'CLAUDE_CODE') {
|
|
2862
|
+
agentResult = await runClaude({
|
|
2031
2863
|
workspaceDir,
|
|
2032
2864
|
binDir,
|
|
2033
2865
|
eventDir,
|
|
@@ -2045,6 +2877,27 @@ async function startWorker(options = {}) {
|
|
|
2045
2877
|
activeTerminators.set(task.id, controls.terminate);
|
|
2046
2878
|
},
|
|
2047
2879
|
});
|
|
2880
|
+
}
|
|
2881
|
+
else if (assignment.agent === 'CURSOR_COMPOSER') {
|
|
2882
|
+
const assignmentAgent = assignment.agent;
|
|
2883
|
+
const assignmentModel = assignment.model;
|
|
2884
|
+
const plannedAttemptIndex = assignment.attemptPlan?.options.findIndex((option) => option.agent === assignmentAgent && option.model === assignmentModel);
|
|
2885
|
+
const attemptIndex = typeof plannedAttemptIndex === 'number' && plannedAttemptIndex >= 0
|
|
2886
|
+
? plannedAttemptIndex
|
|
2887
|
+
: 0;
|
|
2888
|
+
await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
|
|
2889
|
+
workerKey,
|
|
2890
|
+
leaseToken,
|
|
2891
|
+
attemptIndex,
|
|
2892
|
+
reason: 'unavailable_runtime',
|
|
2893
|
+
message: 'Cursor Composer noninteractive worker execution is not configured on this worker.',
|
|
2894
|
+
});
|
|
2895
|
+
fenced = true;
|
|
2896
|
+
throw new Error('worker_cursor_composer_runner_not_configured');
|
|
2897
|
+
}
|
|
2898
|
+
else {
|
|
2899
|
+
throw new Error(`unsupported_worker_agent:${assignment.agent}`);
|
|
2900
|
+
}
|
|
2048
2901
|
agentCompletedAt = new Date();
|
|
2049
2902
|
await queueEventDrain().catch((error) => {
|
|
2050
2903
|
handleEventDrainFailure(error);
|
|
@@ -2238,7 +3091,7 @@ async function startWorker(options = {}) {
|
|
|
2238
3091
|
};
|
|
2239
3092
|
try {
|
|
2240
3093
|
let claimBackoffMs = CLAIM_BACKOFF_BASE_MS;
|
|
2241
|
-
while (!shuttingDown) {
|
|
3094
|
+
while (!shuttingDown && !quiescingForUpdate) {
|
|
2242
3095
|
if (fatalTaskError) {
|
|
2243
3096
|
throw fatalTaskError;
|
|
2244
3097
|
}
|
|
@@ -2261,6 +3114,7 @@ async function startWorker(options = {}) {
|
|
|
2261
3114
|
capabilities,
|
|
2262
3115
|
runningTaskCount: activeTaskRuns.size,
|
|
2263
3116
|
}));
|
|
3117
|
+
handleWorkerAction(claim);
|
|
2264
3118
|
claimBackoffMs = CLAIM_BACKOFF_BASE_MS;
|
|
2265
3119
|
}
|
|
2266
3120
|
catch (error) {
|
|
@@ -2274,7 +3128,7 @@ async function startWorker(options = {}) {
|
|
|
2274
3128
|
}
|
|
2275
3129
|
const task = claim.task;
|
|
2276
3130
|
if (!task) {
|
|
2277
|
-
if (shuttingDown) {
|
|
3131
|
+
if (shuttingDown || quiescingForUpdate) {
|
|
2278
3132
|
break;
|
|
2279
3133
|
}
|
|
2280
3134
|
if (options.once) {
|
|
@@ -2312,6 +3166,10 @@ async function startWorker(options = {}) {
|
|
|
2312
3166
|
if (sessionExpired) {
|
|
2313
3167
|
throw new Error(exports.WORKER_SESSION_EXPIRED_MESSAGE);
|
|
2314
3168
|
}
|
|
3169
|
+
if (quiescingForUpdate) {
|
|
3170
|
+
console.log('Worker quiesced for managed update. Exiting for supervisor restart.');
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
2315
3173
|
if (shuttingDown) {
|
|
2316
3174
|
await sendWorkerHealthAlertSafely({
|
|
2317
3175
|
state: 'stopped',
|
|
@@ -2340,6 +3198,10 @@ async function startWorker(options = {}) {
|
|
|
2340
3198
|
}
|
|
2341
3199
|
node_process_1.default.off('SIGINT', signalHandler);
|
|
2342
3200
|
node_process_1.default.off('SIGTERM', signalHandler);
|
|
3201
|
+
if (options.superviseStdin) {
|
|
3202
|
+
node_process_1.default.stdin.off('end', stdinShutdownHandler);
|
|
3203
|
+
node_process_1.default.stdin.off('close', stdinShutdownHandler);
|
|
3204
|
+
}
|
|
2343
3205
|
if (!crashed) {
|
|
2344
3206
|
await sendWorkerHealthAlertSafely({
|
|
2345
3207
|
state: 'stopped',
|
|
@@ -2348,6 +3210,7 @@ async function startWorker(options = {}) {
|
|
|
2348
3210
|
taskId: firstActiveTaskId(),
|
|
2349
3211
|
});
|
|
2350
3212
|
}
|
|
3213
|
+
clearWorkerRuntimeState(target);
|
|
2351
3214
|
}
|
|
2352
3215
|
}, { env: options.env });
|
|
2353
3216
|
}
|