runwork 0.9.2 → 0.9.3
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/dist/agents/__tests__/codex-stats.test.js +6 -0
- package/dist/agents/__tests__/detection.test.d.ts +1 -0
- package/dist/agents/__tests__/detection.test.js +86 -0
- package/dist/agents/__tests__/graceful-degradation.test.js +6 -0
- package/dist/agents/detection.d.ts +6 -5
- package/dist/agents/detection.js +28 -26
- package/dist/agents/registry-data.d.ts +5 -31
- package/dist/agents/registry-data.js +3 -3
- package/dist/commands/__tests__/sync-redetect.test.d.ts +1 -0
- package/dist/commands/__tests__/sync-redetect.test.js +96 -0
- package/dist/commands/setup.js +4 -1
- package/dist/commands/sync.d.ts +26 -0
- package/dist/commands/sync.js +56 -1
- package/dist/devtools/registry-data.d.ts +39 -0
- package/dist/devtools/registry-data.js +39 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/git/credentials.js +16 -5
- package/dist/tools/types.d.ts +47 -0
- package/dist/tools/types.js +8 -0
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
|
@@ -13,6 +13,12 @@ vi.mock('os', () => ({
|
|
|
13
13
|
}));
|
|
14
14
|
vi.mock('child_process', () => ({
|
|
15
15
|
execFileSync: vi.fn(),
|
|
16
|
+
// detection.ts (transitively imported via codex.ts) wraps execFile through
|
|
17
|
+
// util.promisify; provide a callback-shaped stub so module load succeeds.
|
|
18
|
+
execFile: vi.fn((..._args) => {
|
|
19
|
+
const cb = _args[_args.length - 1];
|
|
20
|
+
cb(new Error('ENOENT'));
|
|
21
|
+
}),
|
|
16
22
|
}));
|
|
17
23
|
vi.mock('../../utils/sqlite.js', () => ({
|
|
18
24
|
querySqlite: vi.fn(() => ''),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
// Force a Windows environment for Windows-only detection methods. The
|
|
3
|
+
// `runAgentDetection` function bails out off Windows for `windows-appx` and
|
|
4
|
+
// `windows-start-app`, so we have to pretend.
|
|
5
|
+
vi.mock('os', async () => {
|
|
6
|
+
const actual = await vi.importActual('os');
|
|
7
|
+
return { ...actual, platform: vi.fn(() => 'win32') };
|
|
8
|
+
});
|
|
9
|
+
const execBehaviors = [];
|
|
10
|
+
let defaultBehavior = { kind: 'success' };
|
|
11
|
+
vi.mock('child_process', () => ({
|
|
12
|
+
// promisify(execFile) drives this: the last argument is always the
|
|
13
|
+
// (err, stdout, stderr) callback regardless of whether options are passed.
|
|
14
|
+
execFile: (...args) => {
|
|
15
|
+
const cb = args[args.length - 1];
|
|
16
|
+
const behavior = execBehaviors.shift() ?? defaultBehavior;
|
|
17
|
+
const finish = (b) => {
|
|
18
|
+
if (b.kind === 'success')
|
|
19
|
+
cb(null, '', '');
|
|
20
|
+
else if (b.kind === 'fail')
|
|
21
|
+
cb(new Error('exit 1'), '', '');
|
|
22
|
+
};
|
|
23
|
+
if (behavior.kind === 'delay') {
|
|
24
|
+
setTimeout(() => finish({ kind: behavior.result }), behavior.ms);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
finish(behavior);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
vi.mock('../../utils/which.js', () => ({
|
|
32
|
+
whichBinary: vi.fn((name) => (name === 'present-binary' ? `/usr/bin/${name}` : null)),
|
|
33
|
+
}));
|
|
34
|
+
import { runAgentDetection } from '../detection.js';
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
execBehaviors.length = 0;
|
|
38
|
+
defaultBehavior = { kind: 'success' };
|
|
39
|
+
});
|
|
40
|
+
describe('runAgentDetection', () => {
|
|
41
|
+
it('reports binary detection via whichBinary', async () => {
|
|
42
|
+
expect(await runAgentDetection({ method: 'binary', target: 'present-binary' })).toBe(true);
|
|
43
|
+
expect(await runAgentDetection({ method: 'binary', target: 'missing-binary' })).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
it('reports false when a windows-appx probe exits non-zero', async () => {
|
|
46
|
+
defaultBehavior = { kind: 'fail' };
|
|
47
|
+
expect(await runAgentDetection({ method: 'windows-appx', target: 'Some.Package' })).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it('reports true when any windows-appx probe succeeds', async () => {
|
|
50
|
+
// First two probes fail, third succeeds — the function should still
|
|
51
|
+
// return true because we union with Promise.all + .some(Boolean).
|
|
52
|
+
execBehaviors.push({ kind: 'fail' }, { kind: 'fail' }, { kind: 'success' });
|
|
53
|
+
expect(await runAgentDetection({ method: 'windows-appx', target: ['A', 'B', 'C'] })).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it('runs windows-appx probes in parallel (wall time ≈ slowest probe, not sum)', async () => {
|
|
56
|
+
// Each probe takes 80ms. Six probes serialized = ~480ms; parallel = ~80ms.
|
|
57
|
+
// Allow slack for CI noise but enough to catch a regression to sync execution.
|
|
58
|
+
defaultBehavior = { kind: 'delay', ms: 80, result: 'fail' };
|
|
59
|
+
const start = Date.now();
|
|
60
|
+
const result = await runAgentDetection({
|
|
61
|
+
method: 'windows-appx',
|
|
62
|
+
target: ['A', 'B', 'C', 'D', 'E', 'F'],
|
|
63
|
+
});
|
|
64
|
+
const elapsed = Date.now() - start;
|
|
65
|
+
expect(result).toBe(false);
|
|
66
|
+
expect(elapsed).toBeLessThan(300);
|
|
67
|
+
});
|
|
68
|
+
it('any combinator returns true on first hit', async () => {
|
|
69
|
+
expect(await runAgentDetection({
|
|
70
|
+
method: 'any',
|
|
71
|
+
target: [
|
|
72
|
+
{ method: 'binary', target: 'missing-binary' },
|
|
73
|
+
{ method: 'binary', target: 'present-binary' },
|
|
74
|
+
],
|
|
75
|
+
})).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it('any combinator returns false when every probe misses', async () => {
|
|
78
|
+
expect(await runAgentDetection({
|
|
79
|
+
method: 'any',
|
|
80
|
+
target: [
|
|
81
|
+
{ method: 'binary', target: 'missing-binary' },
|
|
82
|
+
{ method: 'binary', target: 'also-missing' },
|
|
83
|
+
],
|
|
84
|
+
})).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -13,6 +13,12 @@ vi.mock('os', () => ({
|
|
|
13
13
|
}));
|
|
14
14
|
vi.mock('child_process', () => ({
|
|
15
15
|
execFileSync: vi.fn(() => { throw new Error('ENOENT'); }),
|
|
16
|
+
// detection.ts wraps execFile via util.promisify; provide the callback
|
|
17
|
+
// shape so the import doesn't blow up even though no test here exercises it.
|
|
18
|
+
execFile: vi.fn((..._args) => {
|
|
19
|
+
const cb = _args[_args.length - 1];
|
|
20
|
+
cb(new Error('ENOENT'));
|
|
21
|
+
}),
|
|
16
22
|
}));
|
|
17
23
|
vi.mock('../../utils/sqlite.js', () => ({
|
|
18
24
|
querySqlite: vi.fn(() => ''),
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { AgentDetection } from './registry-data.js';
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Detection — returns true when the agent is installed according to its
|
|
13
|
+
* registry `detection` rules. Supports `binary`, `path`, `windows-appx`,
|
|
14
|
+
* `windows-start-app`, and nested `any` combinators. PowerShell-backed
|
|
15
|
+
* Windows probes run in parallel so a multi-target `any { windows-appx }`
|
|
16
|
+
* pays one PowerShell startup, not N.
|
|
16
17
|
*/
|
|
17
|
-
export declare function runAgentDetection(detection: AgentDetection): boolean
|
|
18
|
+
export declare function runAgentDetection(detection: AgentDetection): Promise<boolean>;
|
package/dist/agents/detection.js
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
* Mirrors the desktop's agent-detection.ts logic but uses Node APIs instead
|
|
8
8
|
* of Tauri plugins.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { execFile } from 'child_process';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
12
|
import { homedir, platform } from 'os';
|
|
13
13
|
import { isAbsolute, join } from 'path';
|
|
14
|
+
import { promisify } from 'util';
|
|
14
15
|
import { whichBinary } from '../utils/which.js';
|
|
15
16
|
import { resolvePlatformString } from './registry.js';
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
16
18
|
function isWindows() {
|
|
17
19
|
return platform() === 'win32';
|
|
18
20
|
}
|
|
@@ -22,9 +24,9 @@ function powershellQuote(value) {
|
|
|
22
24
|
function toList(value) {
|
|
23
25
|
return Array.isArray(value) ? value : [value];
|
|
24
26
|
}
|
|
25
|
-
function runPowerShell(script) {
|
|
27
|
+
async function runPowerShell(script) {
|
|
26
28
|
try {
|
|
27
|
-
|
|
29
|
+
await execFileAsync('powershell', ['-NoProfile', '-Command', script]);
|
|
28
30
|
return true;
|
|
29
31
|
}
|
|
30
32
|
catch {
|
|
@@ -39,33 +41,35 @@ function checkPath(target) {
|
|
|
39
41
|
return existsSync(resolved);
|
|
40
42
|
return existsSync(join(homedir(), resolved));
|
|
41
43
|
}
|
|
42
|
-
function checkWindowsAppxPackage(target) {
|
|
44
|
+
async function checkWindowsAppxPackage(target) {
|
|
43
45
|
if (!isWindows())
|
|
44
46
|
return false;
|
|
45
|
-
|
|
47
|
+
// Probe all candidate package names in parallel; resolve true on first hit.
|
|
48
|
+
const probes = toList(target).map((pkg) => {
|
|
46
49
|
const script = `$p = Get-AppxPackage -Name ${powershellQuote(pkg)} -ErrorAction SilentlyContinue; if ($null -ne $p) { exit 0 } exit 1`;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return
|
|
50
|
+
return runPowerShell(script);
|
|
51
|
+
});
|
|
52
|
+
const results = await Promise.all(probes);
|
|
53
|
+
return results.some(Boolean);
|
|
51
54
|
}
|
|
52
|
-
function checkWindowsStartApp(target) {
|
|
55
|
+
async function checkWindowsStartApp(target) {
|
|
53
56
|
if (!isWindows())
|
|
54
57
|
return false;
|
|
55
|
-
|
|
58
|
+
const probes = toList(target).map((pattern) => {
|
|
56
59
|
const script = `$a = Get-StartApps -Name ${powershellQuote(pattern)} -ErrorAction SilentlyContinue | Select-Object -First 1; if ($null -ne $a) { exit 0 } exit 1`;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return
|
|
60
|
+
return runPowerShell(script);
|
|
61
|
+
});
|
|
62
|
+
const results = await Promise.all(probes);
|
|
63
|
+
return results.some(Boolean);
|
|
61
64
|
}
|
|
62
65
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
66
|
+
* Detection — returns true when the agent is installed according to its
|
|
67
|
+
* registry `detection` rules. Supports `binary`, `path`, `windows-appx`,
|
|
68
|
+
* `windows-start-app`, and nested `any` combinators. PowerShell-backed
|
|
69
|
+
* Windows probes run in parallel so a multi-target `any { windows-appx }`
|
|
70
|
+
* pays one PowerShell startup, not N.
|
|
67
71
|
*/
|
|
68
|
-
export function runAgentDetection(detection) {
|
|
72
|
+
export async function runAgentDetection(detection) {
|
|
69
73
|
switch (detection.method) {
|
|
70
74
|
case 'binary': {
|
|
71
75
|
const target = resolvePlatformString(detection.target);
|
|
@@ -77,12 +81,10 @@ export function runAgentDetection(detection) {
|
|
|
77
81
|
return checkWindowsAppxPackage(detection.target);
|
|
78
82
|
case 'windows-start-app':
|
|
79
83
|
return checkWindowsStartApp(detection.target);
|
|
80
|
-
case 'any':
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
return false;
|
|
84
|
+
case 'any': {
|
|
85
|
+
const probes = await Promise.all(detection.target.map((p) => runAgentDetection(p)));
|
|
86
|
+
return probes.some(Boolean);
|
|
87
|
+
}
|
|
86
88
|
default:
|
|
87
89
|
return false;
|
|
88
90
|
}
|
|
@@ -9,28 +9,10 @@
|
|
|
9
9
|
* imports. Node-only path resolvers live next to this file in `./registry.ts`.
|
|
10
10
|
*/
|
|
11
11
|
export type AgentCategory = 'cli' | 'ide' | 'desktop' | 'extension';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
windows?: string;
|
|
17
|
-
linux?: string;
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* Detection strategy for an agent. Supports simple binary/path checks plus
|
|
21
|
-
* richer Windows-specific methods and a nested `any` combinator used by the
|
|
22
|
-
* desktop app to detect agents that ship as both GUI app and CLI binary.
|
|
23
|
-
*/
|
|
24
|
-
export type AgentDetection = {
|
|
25
|
-
method: 'binary' | 'path';
|
|
26
|
-
target: PlatformString;
|
|
27
|
-
} | {
|
|
28
|
-
method: 'windows-appx' | 'windows-start-app';
|
|
29
|
-
target: string | string[];
|
|
30
|
-
} | {
|
|
31
|
-
method: 'any';
|
|
32
|
-
target: AgentDetection[];
|
|
33
|
-
};
|
|
12
|
+
export type { PlatformString, Detection, InstallableTool } from '../tools/types.js';
|
|
13
|
+
import type { InstallableTool, Detection, PlatformString } from '../tools/types.js';
|
|
14
|
+
/** @deprecated use `Detection` from ../tools/types — kept as alias for back-compat. */
|
|
15
|
+
export type AgentDetection = Detection;
|
|
34
16
|
export interface AgentLaunch {
|
|
35
17
|
app?: {
|
|
36
18
|
macos?: string;
|
|
@@ -70,19 +52,11 @@ export interface AgentManualSetup {
|
|
|
70
52
|
showAfter?: ManualSetupSlot;
|
|
71
53
|
downloadArtifact?: AgentManualSetupArtifact;
|
|
72
54
|
}
|
|
73
|
-
export interface AgentDefinition {
|
|
74
|
-
slug: string;
|
|
75
|
-
name: string;
|
|
55
|
+
export interface AgentDefinition extends InstallableTool {
|
|
76
56
|
aliases?: string[];
|
|
77
|
-
description: string;
|
|
78
57
|
category: AgentCategory;
|
|
79
|
-
detection: AgentDetection;
|
|
80
58
|
/** Desktop launch hints (GUI app name, CLI command) */
|
|
81
59
|
launch?: AgentLaunch;
|
|
82
|
-
/** Logo identifier used by the desktop UI */
|
|
83
|
-
logo?: string;
|
|
84
|
-
/** Public download/install URL shown in the desktop onboarding */
|
|
85
|
-
downloadUrl?: string;
|
|
86
60
|
/** Whether the CLI knows how to auto-install this agent */
|
|
87
61
|
autoInstallable?: boolean;
|
|
88
62
|
/** Skill file directories relative to $HOME (global) or project root (project) */
|
|
@@ -316,7 +316,7 @@ const AGENT_REGISTRY = [
|
|
|
316
316
|
mcpConfigKey: 'mcpServers',
|
|
317
317
|
},
|
|
318
318
|
// === Community agents (from skillshare targets.yaml) ===
|
|
319
|
-
{ slug: 'antigravity', name: 'Antigravity', aliases: ['Antigravity (Google)'], description: "Google's Antigravity AI agent", category: 'cli', detection: { method: 'binary', target: 'antigravity' }, logo: 'antigravity', skillsPaths: { global: '.gemini/antigravity/skills', project: '.agent/skills' } },
|
|
319
|
+
{ slug: 'antigravity', name: 'Antigravity', aliases: ['Antigravity (Google)'], description: "Google's Antigravity AI agent", category: 'cli', detection: { method: 'binary', target: 'antigravity' }, logo: 'antigravity', launch: { cli: 'antigravity' }, skillsPaths: { global: '.gemini/antigravity/skills', project: '.agent/skills' } },
|
|
320
320
|
{ slug: 'amp', name: 'Amp', description: 'AI coding agent by Sourcegraph', category: 'cli', detection: { method: 'binary', target: 'amp' }, skillsPaths: { global: '.config/agents/skills', project: '.agents/skills' } },
|
|
321
321
|
{ slug: 'adal', name: 'AdaL', description: 'AI coding agent', category: 'cli', detection: { method: 'binary', target: 'adal' }, skillsPaths: { global: '.adal/skills', project: '.adal/skills' } },
|
|
322
322
|
{ slug: 'astrbot', name: 'AstrBot', description: 'AI coding agent', category: 'cli', detection: { method: 'binary', target: 'astrbot' }, skillsPaths: { global: '.astrbot/data/skills', project: 'data/skills' } },
|
|
@@ -346,7 +346,7 @@ const AGENT_REGISTRY = [
|
|
|
346
346
|
{ slug: 'neovate', name: 'Neovate', description: 'AI coding agent for Neovim', category: 'extension', detection: { method: 'binary', target: 'neovate' }, skillsPaths: { global: '.neovate/skills', project: '.neovate/skills' } },
|
|
347
347
|
{ slug: 'omp', name: 'Oh My Pi', aliases: ['oh-my-pi'], description: 'AI coding agent', category: 'cli', detection: { method: 'binary', target: 'omp' }, skillsPaths: { global: '.omp/agent/skills', project: '.omp/skills' } },
|
|
348
348
|
{ slug: 'openclaw', name: 'OpenClaw', description: 'AI coding agent', category: 'cli', detection: { method: 'binary', target: 'openclaw' }, skillsPaths: { global: '.openclaw/skills', project: 'skills' } },
|
|
349
|
-
{ slug: 'opencode', name: 'OpenCode', description: 'Open-source AI coding agent', category: 'cli', detection: { method: 'binary', target: 'opencode' }, skillsPaths: { global: '.config/opencode/skills', project: '.opencode/skills' } },
|
|
349
|
+
{ slug: 'opencode', name: 'OpenCode', description: 'Open-source AI coding agent', category: 'cli', detection: { method: 'binary', target: 'opencode' }, launch: { cli: 'opencode' }, skillsPaths: { global: '.config/opencode/skills', project: '.opencode/skills' } },
|
|
350
350
|
{ slug: 'openhands', name: 'OpenHands', description: 'Open-source AI coding agent', category: 'cli', detection: { method: 'binary', target: 'openhands' }, skillsPaths: { global: '.openhands/skills', project: '.openhands/skills' } },
|
|
351
351
|
{ slug: 'pi', name: 'Pi', description: 'AI coding agent', category: 'cli', detection: { method: 'binary', target: 'pi' }, skillsPaths: { global: '.pi/agent/skills', project: '.pi/skills' } },
|
|
352
352
|
{ slug: 'pochi', name: 'Pochi', description: 'AI coding agent', category: 'cli', detection: { method: 'binary', target: 'pochi' }, skillsPaths: { global: '.pochi/skills', project: '.pochi/skills' } },
|
|
@@ -358,7 +358,7 @@ const AGENT_REGISTRY = [
|
|
|
358
358
|
{ slug: 'trae-cn', name: 'Trae CN', description: 'ByteDance AI coding IDE (China)', category: 'ide', detection: { method: 'binary', target: 'trae-cn' }, skillsPaths: { global: '.trae-cn/skills', project: '.trae/skills' } },
|
|
359
359
|
{ slug: 'mistral-vibe', name: 'Mistral Vibe', aliases: ['vibe'], description: "Mistral's AI coding agent", category: 'cli', detection: { method: 'binary', target: 'vibe' }, skillsPaths: { global: '.vibe/skills', project: '.vibe/skills' } },
|
|
360
360
|
{ slug: 'verdent', name: 'Verdent', description: 'AI coding agent', category: 'cli', detection: { method: 'binary', target: 'verdent' }, skillsPaths: { global: '.verdent/skills', project: '.verdent/skills' } },
|
|
361
|
-
{ slug: 'warp', name: 'Warp AI', description: 'AI-powered terminal', category: 'cli', detection: { method: 'path', target: { macos: '/Applications/Warp.app', linux: '/usr/bin/warp-terminal' } }, skillsPaths: { global: '.agents/skills', project: '.agents/skills' } },
|
|
361
|
+
{ slug: 'warp', name: 'Warp Terminal', aliases: ['Warp AI'], description: 'AI-powered terminal', category: 'cli', detection: { method: 'path', target: { macos: '/Applications/Warp.app', linux: '/usr/bin/warp-terminal' } }, launch: { app: { macos: 'Warp', windows: 'Warp' } }, skillsPaths: { global: '.agents/skills', project: '.agents/skills' } },
|
|
362
362
|
{ slug: 'witsy', name: 'Witsy', description: 'AI coding agent', category: 'cli', detection: { method: 'binary', target: 'witsy' }, skillsPaths: { global: '.agents/skills', project: '.agents/skills' } },
|
|
363
363
|
{ slug: 'xcode-claude', name: 'Xcode Claude', description: 'Claude integration for Xcode', category: 'extension', detection: { method: 'path', target: { macos: 'Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig' } }, logo: 'claude', skillsPaths: { global: 'Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig/skills', project: '.claude/skills' } },
|
|
364
364
|
{ slug: 'xcode-codex', name: 'Xcode Codex', description: 'Codex integration for Xcode', category: 'extension', detection: { method: 'path', target: { macos: 'Library/Developer/Xcode/CodingAssistant/codex' } }, logo: 'openai', skillsPaths: { global: 'Library/Developer/Xcode/CodingAssistant/codex/skills', project: '.codex/skills' } },
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('../../agents/detect.js', () => ({
|
|
3
|
+
detectAgents: vi.fn(),
|
|
4
|
+
// getAdapterBySlug is imported by sync.ts at module load; stub it so the
|
|
5
|
+
// import graph resolves without dragging in real adapter side-effects.
|
|
6
|
+
getAdapterBySlug: vi.fn(() => undefined),
|
|
7
|
+
}));
|
|
8
|
+
import { detectAgents } from '../../agents/detect.js';
|
|
9
|
+
import { shouldRedetect, refreshConfiguredAgents, REDETECT_TTL_MS, } from '../sync.js';
|
|
10
|
+
const baseOpts = {
|
|
11
|
+
dryRun: false,
|
|
12
|
+
pullOnly: false,
|
|
13
|
+
yes: true,
|
|
14
|
+
};
|
|
15
|
+
function makeState(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
workspaceId: 'ws-1',
|
|
18
|
+
workspaceName: 'Test',
|
|
19
|
+
configuredAgents: ['claude-code'],
|
|
20
|
+
scope: 'user',
|
|
21
|
+
lastSyncAt: '',
|
|
22
|
+
mcpServers: [],
|
|
23
|
+
skills: [],
|
|
24
|
+
skillHashes: {},
|
|
25
|
+
lastDetectedAt: new Date().toISOString(),
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function fakeAdapter(slug) {
|
|
30
|
+
return {
|
|
31
|
+
name: slug,
|
|
32
|
+
slug,
|
|
33
|
+
detect: async () => true,
|
|
34
|
+
writeMcpServers: async () => { },
|
|
35
|
+
writeSkills: async () => { },
|
|
36
|
+
writeInstructionHint: async () => { },
|
|
37
|
+
supportsSkills: () => false,
|
|
38
|
+
supportsMcpScope: () => false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
vi.clearAllMocks();
|
|
43
|
+
});
|
|
44
|
+
describe('shouldRedetect', () => {
|
|
45
|
+
it('returns true when --redetect is passed even on fresh state', () => {
|
|
46
|
+
expect(shouldRedetect(makeState(), { ...baseOpts, redetect: true })).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
it('returns true when configuredAgents is empty (recovery)', () => {
|
|
49
|
+
expect(shouldRedetect(makeState({ configuredAgents: [] }), baseOpts)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it('returns true when lastDetectedAt is missing (first sync)', () => {
|
|
52
|
+
expect(shouldRedetect(makeState({ lastDetectedAt: undefined }), baseOpts)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it('returns true when lastDetectedAt is unparseable (corrupt state)', () => {
|
|
55
|
+
expect(shouldRedetect(makeState({ lastDetectedAt: 'not-a-date' }), baseOpts)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it('returns true when lastDetectedAt is older than the TTL', () => {
|
|
58
|
+
const stale = new Date(Date.now() - REDETECT_TTL_MS - 1000).toISOString();
|
|
59
|
+
expect(shouldRedetect(makeState({ lastDetectedAt: stale }), baseOpts)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
it('returns false when lastDetectedAt is fresh (within TTL)', () => {
|
|
62
|
+
const fresh = new Date(Date.now() - 60_000).toISOString();
|
|
63
|
+
expect(shouldRedetect(makeState({ lastDetectedAt: fresh }), baseOpts)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('refreshConfiguredAgents', () => {
|
|
67
|
+
it('appends newly-detected agents and returns the diff', async () => {
|
|
68
|
+
vi.mocked(detectAgents).mockResolvedValue([
|
|
69
|
+
fakeAdapter('claude-code'),
|
|
70
|
+
fakeAdapter('codex'),
|
|
71
|
+
fakeAdapter('cursor'),
|
|
72
|
+
]);
|
|
73
|
+
const state = makeState({ configuredAgents: ['claude-code'], lastDetectedAt: undefined });
|
|
74
|
+
const added = await refreshConfiguredAgents(state);
|
|
75
|
+
expect(added.sort()).toEqual(['codex', 'cursor']);
|
|
76
|
+
expect(state.configuredAgents.sort()).toEqual(['claude-code', 'codex', 'cursor']);
|
|
77
|
+
expect(state.lastDetectedAt).toBeTruthy();
|
|
78
|
+
});
|
|
79
|
+
it('returns empty diff and stamps lastDetectedAt when nothing new is found', async () => {
|
|
80
|
+
vi.mocked(detectAgents).mockResolvedValue([fakeAdapter('claude-code')]);
|
|
81
|
+
const state = makeState({ configuredAgents: ['claude-code'], lastDetectedAt: undefined });
|
|
82
|
+
const added = await refreshConfiguredAgents(state);
|
|
83
|
+
expect(added).toEqual([]);
|
|
84
|
+
expect(state.configuredAgents).toEqual(['claude-code']);
|
|
85
|
+
expect(state.lastDetectedAt).toBeTruthy();
|
|
86
|
+
});
|
|
87
|
+
it('never removes agents that are no longer detected', async () => {
|
|
88
|
+
// A transient PATH glitch or temporarily-unmounted disk shouldn't wipe
|
|
89
|
+
// an agent the user explicitly configured. Removal is opt-in via setup.
|
|
90
|
+
vi.mocked(detectAgents).mockResolvedValue([fakeAdapter('claude-code')]);
|
|
91
|
+
const state = makeState({ configuredAgents: ['claude-code', 'codex', 'cursor'] });
|
|
92
|
+
const added = await refreshConfiguredAgents(state);
|
|
93
|
+
expect(added).toEqual([]);
|
|
94
|
+
expect(state.configuredAgents.sort()).toEqual(['claude-code', 'codex', 'cursor']);
|
|
95
|
+
});
|
|
96
|
+
});
|
package/dist/commands/setup.js
CHANGED
|
@@ -110,7 +110,9 @@ export const setupCommand = new Command('setup')
|
|
|
110
110
|
const chosen = await promptSelect('Configure for:', scopeChoices);
|
|
111
111
|
scope = chosen.value;
|
|
112
112
|
}
|
|
113
|
-
// 5. Save setup state (minimal, sync populates the rest)
|
|
113
|
+
// 5. Save setup state (minimal, sync populates the rest).
|
|
114
|
+
// `lastDetectedAt` is stamped now so the immediate post-setup sync
|
|
115
|
+
// doesn't waste time re-running detection that just ran.
|
|
114
116
|
const state = {
|
|
115
117
|
workspaceId,
|
|
116
118
|
workspaceName: workspaceName || '',
|
|
@@ -121,6 +123,7 @@ export const setupCommand = new Command('setup')
|
|
|
121
123
|
mcpServers: [],
|
|
122
124
|
skills: [],
|
|
123
125
|
skillHashes: {},
|
|
126
|
+
lastDetectedAt: new Date().toISOString(),
|
|
124
127
|
};
|
|
125
128
|
const scopes = scope === 'both' ? ['project', 'user'] : [scope];
|
|
126
129
|
for (const s of scopes) {
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -6,6 +6,32 @@ export interface SyncOptions {
|
|
|
6
6
|
prefer?: 'local' | 'remote';
|
|
7
7
|
yes: boolean;
|
|
8
8
|
verbose?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Force a fresh agent-installation detection pass and union newly-found
|
|
11
|
+
* agents into `state.configuredAgents`. Without this, sync only re-detects
|
|
12
|
+
* when state is empty or `lastDetectedAt` is older than REDETECT_TTL_MS.
|
|
13
|
+
*/
|
|
14
|
+
redetect?: boolean;
|
|
9
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* How long a detection result stays trusted before sync re-runs detection
|
|
18
|
+
* on its own. Detection is cheap on macOS/Linux but pays a PowerShell
|
|
19
|
+
* waterfall on Windows, so we cache for a day. Users who just installed
|
|
20
|
+
* a new agent can shortcut the wait via `--redetect` (or the desktop's
|
|
21
|
+
* "Rescan" button, which forwards the flag).
|
|
22
|
+
*/
|
|
23
|
+
export declare const REDETECT_TTL_MS: number;
|
|
24
|
+
/**
|
|
25
|
+
* Decide whether to re-run installation detection for this sync. We always
|
|
26
|
+
* re-detect on explicit user request, on first sync (no timestamp yet), when
|
|
27
|
+
* setup ended up with zero agents (recovery), or after the TTL has lapsed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function shouldRedetect(state: SetupState, opts: SyncOptions): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Detect installed agents and merge any new slugs into `state.configuredAgents`.
|
|
32
|
+
* Never removes agents — a transient PATH glitch shouldn't wipe the setup.
|
|
33
|
+
* Returns the slugs that were newly added (for logging).
|
|
34
|
+
*/
|
|
35
|
+
export declare function refreshConfiguredAgents(state: SetupState): Promise<string[]>;
|
|
10
36
|
export declare function syncFromState(state: SetupState, statePath: string, credentials: Credentials, opts: SyncOptions): Promise<void>;
|
|
11
37
|
export declare const syncCommand: Command;
|
package/dist/commands/sync.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join } from 'path';
|
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { requireAuth } from '../auth/store.js';
|
|
6
6
|
import { ApiClient } from '../api/client.js';
|
|
7
|
-
import { getAdapterBySlug } from '../agents/detect.js';
|
|
7
|
+
import { getAdapterBySlug, detectAgents } from '../agents/detect.js';
|
|
8
8
|
import { CodexAdapter } from '../agents/codex.js';
|
|
9
9
|
import { RUNWORK_MCP_PREFIX, RUNWORK_WORKSPACE_MCP_NAME } from '../agents/types.js';
|
|
10
10
|
import { collectTelemetryEvents, printTelemetryVerbose, summarizeTelemetryForDryRun, } from './sync-telemetry.js';
|
|
@@ -60,6 +60,49 @@ function readLocalSkills(state) {
|
|
|
60
60
|
}
|
|
61
61
|
return results;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* How long a detection result stays trusted before sync re-runs detection
|
|
65
|
+
* on its own. Detection is cheap on macOS/Linux but pays a PowerShell
|
|
66
|
+
* waterfall on Windows, so we cache for a day. Users who just installed
|
|
67
|
+
* a new agent can shortcut the wait via `--redetect` (or the desktop's
|
|
68
|
+
* "Rescan" button, which forwards the flag).
|
|
69
|
+
*/
|
|
70
|
+
export const REDETECT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
71
|
+
/**
|
|
72
|
+
* Decide whether to re-run installation detection for this sync. We always
|
|
73
|
+
* re-detect on explicit user request, on first sync (no timestamp yet), when
|
|
74
|
+
* setup ended up with zero agents (recovery), or after the TTL has lapsed.
|
|
75
|
+
*/
|
|
76
|
+
export function shouldRedetect(state, opts) {
|
|
77
|
+
if (opts.redetect)
|
|
78
|
+
return true;
|
|
79
|
+
if (state.configuredAgents.length === 0)
|
|
80
|
+
return true;
|
|
81
|
+
if (!state.lastDetectedAt)
|
|
82
|
+
return true;
|
|
83
|
+
const last = Date.parse(state.lastDetectedAt);
|
|
84
|
+
if (Number.isNaN(last))
|
|
85
|
+
return true;
|
|
86
|
+
return Date.now() - last > REDETECT_TTL_MS;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Detect installed agents and merge any new slugs into `state.configuredAgents`.
|
|
90
|
+
* Never removes agents — a transient PATH glitch shouldn't wipe the setup.
|
|
91
|
+
* Returns the slugs that were newly added (for logging).
|
|
92
|
+
*/
|
|
93
|
+
export async function refreshConfiguredAgents(state) {
|
|
94
|
+
const detected = await detectAgents();
|
|
95
|
+
const before = new Set(state.configuredAgents);
|
|
96
|
+
const added = [];
|
|
97
|
+
for (const adapter of detected) {
|
|
98
|
+
if (!before.has(adapter.slug)) {
|
|
99
|
+
state.configuredAgents.push(adapter.slug);
|
|
100
|
+
added.push(adapter.slug);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
state.lastDetectedAt = new Date().toISOString();
|
|
104
|
+
return added;
|
|
105
|
+
}
|
|
63
106
|
export async function syncFromState(state, statePath, credentials, opts) {
|
|
64
107
|
const client = new ApiClient(credentials);
|
|
65
108
|
// Refresh workspace name/slug if missing or stale (e.g. setup ran before name was set)
|
|
@@ -77,6 +120,16 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
77
120
|
}
|
|
78
121
|
}
|
|
79
122
|
console.log(`Syncing workspace: ${state.workspaceName || state.workspaceId}`);
|
|
123
|
+
// Refresh the configured-agents list when the cached detection is stale,
|
|
124
|
+
// empty, or the user explicitly asked. This is what catches "I just installed
|
|
125
|
+
// Claude Code on this machine" without needing the user to re-run setup.
|
|
126
|
+
if (shouldRedetect(state, opts)) {
|
|
127
|
+
console.log(' Detecting installed agents...');
|
|
128
|
+
const added = await refreshConfiguredAgents(state);
|
|
129
|
+
if (added.length > 0) {
|
|
130
|
+
console.log(` Detected new agent${added.length > 1 ? 's' : ''}: ${added.join(', ')}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
80
133
|
console.log(' Fetching workspace data...');
|
|
81
134
|
// Fetch latest data (includeContent=true gets all skill content in one request)
|
|
82
135
|
const [allSkills, mcpServers, externalSkills, registries, connectedIntegrations] = await Promise.all([
|
|
@@ -436,6 +489,7 @@ export const syncCommand = new Command('sync')
|
|
|
436
489
|
.option('--pull-only', 'Only pull remote changes, never push local edits')
|
|
437
490
|
.option('--prefer <side>', 'Auto-resolve conflicts: "local" or "remote" (implies --yes)')
|
|
438
491
|
.option('-y, --yes', 'Non-interactive mode (default: prefer remote for conflicts)')
|
|
492
|
+
.option('--redetect', 'Force agent-installation re-detection (picks up agents installed since last setup)')
|
|
439
493
|
.action(async (opts) => {
|
|
440
494
|
const credentials = requireAuth();
|
|
441
495
|
const syncOpts = {
|
|
@@ -444,6 +498,7 @@ export const syncCommand = new Command('sync')
|
|
|
444
498
|
yes: !!opts.yes || !!opts.prefer,
|
|
445
499
|
prefer: opts.prefer,
|
|
446
500
|
verbose: !!opts.verbose,
|
|
501
|
+
redetect: !!opts.redetect,
|
|
447
502
|
};
|
|
448
503
|
const projectStatePath = join(process.cwd(), '.runwork', 'setup.json');
|
|
449
504
|
const userStatePath = join(homedir(), '.runwork', 'setup.json');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Developer tool registry — non-AI prerequisites Runwork users need installed
|
|
3
|
+
* locally (git today, more later if needed).
|
|
4
|
+
*
|
|
5
|
+
* Extends the shared `InstallableTool` base so the desktop can detect and
|
|
6
|
+
* render devtools alongside AI agents using the same icon/detection plumbing.
|
|
7
|
+
*
|
|
8
|
+
* Browser-safe: no node-only imports.
|
|
9
|
+
*/
|
|
10
|
+
import type { InstallableTool } from '../tools/types.js';
|
|
11
|
+
/**
|
|
12
|
+
* How the desktop should attempt to install the tool. Each platform either
|
|
13
|
+
* has an automated install command (run via the platform shell) or falls
|
|
14
|
+
* back to opening the tool's `downloadUrl` in the browser.
|
|
15
|
+
*/
|
|
16
|
+
export type DevToolInstallStrategy = {
|
|
17
|
+
kind: 'shell';
|
|
18
|
+
shell: 'powershell' | 'zsh' | 'bash';
|
|
19
|
+
command: string;
|
|
20
|
+
postInstallNote?: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'open-url';
|
|
23
|
+
url: string;
|
|
24
|
+
};
|
|
25
|
+
export interface DevToolDefinition extends InstallableTool {
|
|
26
|
+
/** Beginner-friendly explanation shown in the install card. */
|
|
27
|
+
longDescription: string;
|
|
28
|
+
/**
|
|
29
|
+
* Per-platform install strategy. Missing platforms fall back to opening
|
|
30
|
+
* `downloadUrl` in the browser.
|
|
31
|
+
*/
|
|
32
|
+
install: {
|
|
33
|
+
macos?: DevToolInstallStrategy;
|
|
34
|
+
windows?: DevToolInstallStrategy;
|
|
35
|
+
linux?: DevToolInstallStrategy;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export declare function getDevTools(): DevToolDefinition[];
|
|
39
|
+
export declare function getDevTool(slug: string): DevToolDefinition | undefined;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Developer tool registry — non-AI prerequisites Runwork users need installed
|
|
3
|
+
* locally (git today, more later if needed).
|
|
4
|
+
*
|
|
5
|
+
* Extends the shared `InstallableTool` base so the desktop can detect and
|
|
6
|
+
* render devtools alongside AI agents using the same icon/detection plumbing.
|
|
7
|
+
*
|
|
8
|
+
* Browser-safe: no node-only imports.
|
|
9
|
+
*/
|
|
10
|
+
const DEV_TOOL_REGISTRY = [
|
|
11
|
+
{
|
|
12
|
+
slug: 'git',
|
|
13
|
+
name: 'Git',
|
|
14
|
+
description: 'Version control system',
|
|
15
|
+
longDescription: 'Git tracks changes to your code and is required for Runwork commands that sync your work with the platform (init, clone, dev, deploy).',
|
|
16
|
+
detection: { method: 'binary', target: 'git' },
|
|
17
|
+
downloadUrl: 'https://git-scm.com/downloads',
|
|
18
|
+
install: {
|
|
19
|
+
windows: {
|
|
20
|
+
kind: 'shell',
|
|
21
|
+
shell: 'powershell',
|
|
22
|
+
command: 'winget install --id Git.Git -e --source winget --accept-package-agreements --accept-source-agreements',
|
|
23
|
+
},
|
|
24
|
+
macos: {
|
|
25
|
+
kind: 'shell',
|
|
26
|
+
shell: 'zsh',
|
|
27
|
+
command: 'xcode-select --install',
|
|
28
|
+
postInstallNote: 'A macOS installer window has opened. Finish the install there, then click Re-detect.',
|
|
29
|
+
},
|
|
30
|
+
linux: { kind: 'open-url', url: 'https://git-scm.com/download/linux' },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
export function getDevTools() {
|
|
35
|
+
return DEV_TOOL_REGISTRY;
|
|
36
|
+
}
|
|
37
|
+
export function getDevTool(slug) {
|
|
38
|
+
return DEV_TOOL_REGISTRY.find((t) => t.slug === slug);
|
|
39
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.9.
|
|
1
|
+
export declare const VERSION = "0.9.3";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/embed-types.ts -- do not edit
|
|
2
|
-
export const VERSION = "0.9.
|
|
2
|
+
export const VERSION = "0.9.3";
|
package/dist/git/credentials.js
CHANGED
|
@@ -6,11 +6,22 @@ import { getCredentials } from '../auth/store.js';
|
|
|
6
6
|
*/
|
|
7
7
|
export async function configureGitCredentials(remoteUrl) {
|
|
8
8
|
const origin = new URL(remoteUrl).origin;
|
|
9
|
-
|
|
10
|
-
'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
try {
|
|
10
|
+
execFileSync('git', [
|
|
11
|
+
'config', '--global',
|
|
12
|
+
`credential.${origin}.helper`,
|
|
13
|
+
'!runwork git-credential-helper',
|
|
14
|
+
], { stdio: 'pipe' });
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
const code = err?.code;
|
|
18
|
+
if (code === 'ENOENT') {
|
|
19
|
+
console.warn('Note: git is not installed. Skipping git credential helper setup.');
|
|
20
|
+
console.warn('Install git before running `runwork init`, `clone`, `dev`, or `deploy`.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
14
25
|
}
|
|
15
26
|
/**
|
|
16
27
|
* Remove the git credential helper configuration for our remote.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for everything Runwork can detect and offer to install
|
|
3
|
+
* locally. Both AI agents and developer prerequisites (git, etc.) extend
|
|
4
|
+
* `InstallableTool` so the desktop can render and detect them uniformly.
|
|
5
|
+
*
|
|
6
|
+
* Browser-safe: no node-only imports.
|
|
7
|
+
*/
|
|
8
|
+
/** Platform-specific string. Plain string = same on all platforms. */
|
|
9
|
+
export type PlatformString = string | {
|
|
10
|
+
default?: string;
|
|
11
|
+
macos?: string;
|
|
12
|
+
windows?: string;
|
|
13
|
+
linux?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Detection strategy. Supports simple binary/path checks plus richer
|
|
17
|
+
* Windows-specific methods and a nested `any` combinator for tools that
|
|
18
|
+
* ship as both GUI app and CLI binary.
|
|
19
|
+
*/
|
|
20
|
+
export type Detection = {
|
|
21
|
+
method: 'binary' | 'path';
|
|
22
|
+
target: PlatformString;
|
|
23
|
+
} | {
|
|
24
|
+
method: 'windows-appx' | 'windows-start-app';
|
|
25
|
+
target: string | string[];
|
|
26
|
+
} | {
|
|
27
|
+
method: 'any';
|
|
28
|
+
target: Detection[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Common shape for anything that can be detected and installed locally.
|
|
32
|
+
* AI agents and developer tools both extend this.
|
|
33
|
+
*/
|
|
34
|
+
export interface InstallableTool {
|
|
35
|
+
/** Stable identifier, kebab-case (e.g. "claude-code", "git"). */
|
|
36
|
+
slug: string;
|
|
37
|
+
/** Display name. */
|
|
38
|
+
name: string;
|
|
39
|
+
/** One-line description shown in lists. */
|
|
40
|
+
description: string;
|
|
41
|
+
/** How to determine whether this tool is installed locally. */
|
|
42
|
+
detection: Detection;
|
|
43
|
+
/** Public install/download URL — universal fallback when auto-install isn't available. */
|
|
44
|
+
downloadUrl?: string;
|
|
45
|
+
/** Logo identifier (matches an entry in the desktop's logo map). Optional. */
|
|
46
|
+
logo?: string;
|
|
47
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for everything Runwork can detect and offer to install
|
|
3
|
+
* locally. Both AI agents and developer prerequisites (git, etc.) extend
|
|
4
|
+
* `InstallableTool` so the desktop can render and detect them uniformly.
|
|
5
|
+
*
|
|
6
|
+
* Browser-safe: no node-only imports.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
package/dist/types.d.ts
CHANGED
|
@@ -141,6 +141,12 @@ export interface SetupState {
|
|
|
141
141
|
}>;
|
|
142
142
|
reportedAgentSlugs?: string[];
|
|
143
143
|
lastHealthReportAt?: string;
|
|
144
|
+
/**
|
|
145
|
+
* ISO timestamp of the last agent-installation detection pass. Sync uses
|
|
146
|
+
* this with a TTL to skip the (Windows-expensive) PowerShell probe waterfall
|
|
147
|
+
* on every cycle while still catching newly-installed agents within a day.
|
|
148
|
+
*/
|
|
149
|
+
lastDetectedAt?: string;
|
|
144
150
|
}
|
|
145
151
|
export interface WorkflowInfo {
|
|
146
152
|
name: string;
|
package/package.json
CHANGED