runwork 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/agents/__tests__/claude-code-stats.test.d.ts +1 -0
  2. package/dist/agents/__tests__/claude-code-stats.test.js +153 -0
  3. package/dist/agents/__tests__/claude-desktop-stats.test.d.ts +1 -0
  4. package/dist/agents/__tests__/claude-desktop-stats.test.js +280 -0
  5. package/dist/agents/__tests__/codex-stats.test.d.ts +1 -0
  6. package/dist/agents/__tests__/codex-stats.test.js +262 -0
  7. package/dist/agents/__tests__/cursor-stats.test.d.ts +1 -0
  8. package/dist/agents/__tests__/cursor-stats.test.js +183 -0
  9. package/dist/agents/__tests__/graceful-degradation.test.d.ts +1 -0
  10. package/dist/agents/__tests__/graceful-degradation.test.js +123 -0
  11. package/dist/agents/__tests__/instruction-hint.test.d.ts +1 -0
  12. package/dist/agents/__tests__/instruction-hint.test.js +69 -0
  13. package/dist/agents/__tests__/intro-skill.test.d.ts +1 -0
  14. package/dist/agents/__tests__/intro-skill.test.js +135 -0
  15. package/dist/agents/__tests__/schema-validation.test.d.ts +1 -0
  16. package/dist/agents/__tests__/schema-validation.test.js +212 -0
  17. package/dist/agents/claude-code.d.ts +16 -1
  18. package/dist/agents/claude-code.js +344 -21
  19. package/dist/agents/claude-desktop.d.ts +10 -1
  20. package/dist/agents/claude-desktop.js +250 -8
  21. package/dist/agents/cline.d.ts +13 -0
  22. package/dist/agents/cline.js +88 -0
  23. package/dist/agents/codex.d.ts +7 -2
  24. package/dist/agents/codex.js +130 -18
  25. package/dist/agents/cursor.d.ts +6 -1
  26. package/dist/agents/cursor.js +164 -7
  27. package/dist/agents/detect.js +30 -7
  28. package/dist/agents/gemini.d.ts +6 -3
  29. package/dist/agents/gemini.js +57 -20
  30. package/dist/agents/generic-adapter.d.ts +23 -0
  31. package/dist/agents/generic-adapter.js +106 -0
  32. package/dist/agents/intro-skill.d.ts +36 -0
  33. package/dist/agents/intro-skill.js +358 -0
  34. package/dist/agents/registry.d.ts +53 -0
  35. package/dist/agents/registry.js +219 -0
  36. package/dist/agents/types.d.ts +44 -1
  37. package/dist/agents/types.js +11 -4
  38. package/dist/agents/utils/instruction-hint.d.ts +11 -0
  39. package/dist/agents/utils/instruction-hint.js +62 -0
  40. package/dist/agents/utils/json-config.js +3 -3
  41. package/dist/agents/windsurf.d.ts +2 -0
  42. package/dist/agents/windsurf.js +26 -9
  43. package/dist/api/client.d.ts +85 -2
  44. package/dist/api/client.js +40 -10
  45. package/dist/auth/__tests__/login-flow.test.d.ts +1 -0
  46. package/dist/auth/__tests__/login-flow.test.js +125 -0
  47. package/dist/auth/login-flow.d.ts +16 -0
  48. package/dist/auth/login-flow.js +101 -1
  49. package/dist/commands/clone.d.ts +9 -1
  50. package/dist/commands/clone.js +40 -12
  51. package/dist/commands/deploy.js +27 -4
  52. package/dist/commands/dev.js +35 -1
  53. package/dist/commands/doctor.d.ts +2 -0
  54. package/dist/commands/doctor.js +82 -0
  55. package/dist/commands/info.js +6 -2
  56. package/dist/commands/init.d.ts +10 -2
  57. package/dist/commands/init.js +58 -15
  58. package/dist/commands/integrations.js +47 -10
  59. package/dist/commands/login.js +31 -4
  60. package/dist/commands/mcp.js +143 -15
  61. package/dist/commands/setup.js +47 -122
  62. package/dist/commands/skills.js +230 -12
  63. package/dist/commands/sync.d.ts +8 -0
  64. package/dist/commands/sync.js +398 -65
  65. package/dist/commands/welcome.js +6 -3
  66. package/dist/generated/version.d.ts +1 -1
  67. package/dist/generated/version.js +1 -1
  68. package/dist/health/__tests__/checks.test.d.ts +1 -0
  69. package/dist/health/__tests__/checks.test.js +223 -0
  70. package/dist/health/checks.d.ts +20 -0
  71. package/dist/health/checks.js +392 -0
  72. package/dist/health/runner.d.ts +2 -0
  73. package/dist/health/runner.js +26 -0
  74. package/dist/health/types.d.ts +12 -0
  75. package/dist/health/types.js +1 -0
  76. package/dist/index.js +32 -0
  77. package/dist/sync/__tests__/change-detect.test.d.ts +1 -0
  78. package/dist/sync/__tests__/change-detect.test.js +123 -0
  79. package/dist/sync/__tests__/hash.test.d.ts +1 -0
  80. package/dist/sync/__tests__/hash.test.js +18 -0
  81. package/dist/sync/change-detect.d.ts +19 -0
  82. package/dist/sync/change-detect.js +136 -0
  83. package/dist/sync/conflict-ui.d.ts +5 -0
  84. package/dist/sync/conflict-ui.js +78 -0
  85. package/dist/sync/executor.d.ts +21 -0
  86. package/dist/sync/executor.js +98 -0
  87. package/dist/sync/hash.d.ts +1 -0
  88. package/dist/sync/hash.js +4 -0
  89. package/dist/sync/types.d.ts +24 -0
  90. package/dist/sync/types.js +1 -0
  91. package/dist/types.d.ts +12 -0
  92. package/dist/ui/__tests__/banner.test.js +17 -1
  93. package/dist/ui/banner.d.ts +1 -1
  94. package/dist/ui/banner.js +27 -2
  95. package/dist/utils/__tests__/resolve.test.d.ts +1 -0
  96. package/dist/utils/__tests__/resolve.test.js +95 -0
  97. package/dist/utils/__tests__/sqlite.test.d.ts +1 -0
  98. package/dist/utils/__tests__/sqlite.test.js +77 -0
  99. package/dist/utils/agent-guidance.d.ts +32 -0
  100. package/dist/utils/agent-guidance.js +106 -0
  101. package/dist/utils/fs.js +2 -2
  102. package/dist/utils/help-json.d.ts +56 -0
  103. package/dist/utils/help-json.js +85 -0
  104. package/dist/utils/resolve.d.ts +12 -0
  105. package/dist/utils/resolve.js +52 -0
  106. package/dist/utils/setup-state.d.ts +3 -0
  107. package/dist/utils/setup-state.js +19 -0
  108. package/dist/utils/sqlite.d.ts +7 -0
  109. package/dist/utils/sqlite.js +24 -0
  110. package/dist/utils/which.d.ts +6 -0
  111. package/dist/utils/which.js +18 -0
  112. package/package.json +7 -3
  113. package/dist/agents/copilot-cli.d.ts +0 -10
  114. package/dist/agents/copilot-cli.js +0 -46
  115. package/dist/agents/copilot-vscode.d.ts +0 -10
  116. package/dist/agents/copilot-vscode.js +0 -37
@@ -0,0 +1,223 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ vi.mock('fs', async (importOriginal) => {
3
+ const actual = await importOriginal();
4
+ return { ...actual, existsSync: vi.fn(() => true) };
5
+ });
6
+ vi.mock('child_process', async (importOriginal) => {
7
+ const actual = await importOriginal();
8
+ return { ...actual, execFileSync: vi.fn() };
9
+ });
10
+ import { existsSync } from 'fs';
11
+ import { execFileSync } from 'child_process';
12
+ const mockedExistsSync = vi.mocked(existsSync);
13
+ const mockedExecFileSync = vi.mocked(execFileSync);
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ vi.restoreAllMocks();
17
+ vi.spyOn(console, 'log').mockImplementation(() => { });
18
+ vi.spyOn(console, 'error').mockImplementation(() => { });
19
+ mockedExistsSync.mockReturnValue(true);
20
+ });
21
+ afterEach(() => {
22
+ vi.restoreAllMocks();
23
+ });
24
+ // ── Helpers ──────────────────────────────────────────────────────────
25
+ function makeCtx(overrides = {}) {
26
+ return {
27
+ credentials: { apiKey: 'key', email: 'user@test.com', baseUrl: 'https://runwork.ai' },
28
+ client: null,
29
+ config: null,
30
+ cwd: '/test/dir',
31
+ ...overrides,
32
+ };
33
+ }
34
+ function makeConfig(overrides = {}) {
35
+ return {
36
+ workspaceId: 'ws-1',
37
+ workspaceName: 'Test WS',
38
+ appId: 'app-1',
39
+ appName: 'Test App',
40
+ ...overrides,
41
+ };
42
+ }
43
+ // ── 1. System Dependencies ───────────────────────────────────────────
44
+ describe('checkSystemDeps', () => {
45
+ it('passes with modern git', async () => {
46
+ mockedExecFileSync.mockReturnValue(Buffer.from('git version 2.43.0'));
47
+ const { checkSystemDeps } = await import('../checks.js');
48
+ const result = await checkSystemDeps();
49
+ expect(result.status).toBe('pass');
50
+ expect(result.message).toContain('2.43.0');
51
+ });
52
+ it('warns with old git', async () => {
53
+ mockedExecFileSync.mockReturnValue(Buffer.from('git version 2.15.0'));
54
+ const { checkSystemDeps } = await import('../checks.js');
55
+ const result = await checkSystemDeps();
56
+ expect(result.status).toBe('warn');
57
+ expect(result.message).toContain('old');
58
+ });
59
+ it('fails when git not found', async () => {
60
+ mockedExecFileSync.mockImplementation(() => { throw new Error('not found'); });
61
+ const { checkSystemDeps } = await import('../checks.js');
62
+ const result = await checkSystemDeps();
63
+ expect(result.status).toBe('fail');
64
+ expect(result.fix).toBeDefined();
65
+ });
66
+ });
67
+ // ── 3 & 4. Auth + Network ───────────────────────────────────────────
68
+ describe('checkAuthAndNetwork', () => {
69
+ it('fails auth when no credentials', async () => {
70
+ const { checkAuthAndNetwork } = await import('../checks.js');
71
+ const ctx = makeCtx({ credentials: null, client: null });
72
+ const { auth, network } = await checkAuthAndNetwork(ctx);
73
+ expect(auth.status).toBe('fail');
74
+ expect(auth.fix).toContain('login');
75
+ expect(network.status).toBe('skip');
76
+ });
77
+ it('passes when API responds', async () => {
78
+ const { checkAuthAndNetwork } = await import('../checks.js');
79
+ const mockClient = {
80
+ listWorkspaces: vi.fn().mockResolvedValue([{ id: 'ws-1', name: 'WS' }]),
81
+ };
82
+ const ctx = makeCtx({ client: mockClient });
83
+ const { auth, network } = await checkAuthAndNetwork(ctx);
84
+ expect(auth.status).toBe('pass');
85
+ expect(auth.message).toContain('user@test.com');
86
+ expect(network.status).toBe('pass');
87
+ });
88
+ it('distinguishes auth error from network error', async () => {
89
+ const { checkAuthAndNetwork } = await import('../checks.js');
90
+ const mockClient = {
91
+ listWorkspaces: vi.fn().mockRejectedValue(new Error('API error 401: Unauthorized')),
92
+ };
93
+ const ctx = makeCtx({ client: mockClient });
94
+ const { auth, network } = await checkAuthAndNetwork(ctx);
95
+ expect(auth.status).toBe('fail');
96
+ expect(network.status).toBe('pass');
97
+ });
98
+ it('fails network on connection error', async () => {
99
+ const { checkAuthAndNetwork } = await import('../checks.js');
100
+ const mockClient = {
101
+ listWorkspaces: vi.fn().mockRejectedValue(new Error('fetch failed')),
102
+ };
103
+ const ctx = makeCtx({ client: mockClient });
104
+ const { auth, network } = await checkAuthAndNetwork(ctx);
105
+ expect(auth.status).toBe('skip');
106
+ expect(network.status).toBe('fail');
107
+ });
108
+ });
109
+ // ── 5. Project Config ────────────────────────────────────────────────
110
+ describe('checkProjectConfig', () => {
111
+ it('passes with valid config', async () => {
112
+ const { checkProjectConfig } = await import('../checks.js');
113
+ mockedExistsSync.mockReturnValue(true);
114
+ const ctx = makeCtx({ config: makeConfig() });
115
+ const result = await checkProjectConfig(ctx);
116
+ expect(result.status).toBe('pass');
117
+ expect(result.message).toContain('Test App');
118
+ expect(result.message).toContain('Test WS');
119
+ });
120
+ it('fails with malformed config (null)', async () => {
121
+ const { checkProjectConfig } = await import('../checks.js');
122
+ mockedExistsSync.mockReturnValue(true);
123
+ const ctx = makeCtx({ config: null });
124
+ const result = await checkProjectConfig(ctx);
125
+ expect(result.status).toBe('fail');
126
+ expect(result.message).toContain('malformed');
127
+ });
128
+ it('passes outside project when default workspace set', async () => {
129
+ const { checkProjectConfig } = await import('../checks.js');
130
+ mockedExistsSync.mockReturnValue(false);
131
+ const creds = {
132
+ apiKey: 'key',
133
+ email: 'user@test.com',
134
+ baseUrl: 'https://runwork.ai',
135
+ defaultWorkspaceId: 'ws-1',
136
+ defaultWorkspaceName: 'My WS',
137
+ };
138
+ const ctx = makeCtx({ credentials: creds, config: null });
139
+ const result = await checkProjectConfig(ctx);
140
+ expect(result.status).toBe('pass');
141
+ expect(result.message).toContain('My WS');
142
+ });
143
+ it('warns outside project when no default workspace', async () => {
144
+ const { checkProjectConfig } = await import('../checks.js');
145
+ mockedExistsSync.mockReturnValue(false);
146
+ const ctx = makeCtx({ config: null });
147
+ const result = await checkProjectConfig(ctx);
148
+ expect(result.status).toBe('warn');
149
+ expect(result.fix).toBeDefined();
150
+ });
151
+ });
152
+ // ── 6. App Exists ────────────────────────────────────────────────────
153
+ describe('checkAppExists', () => {
154
+ it('skips when no config or client', async () => {
155
+ const { checkAppExists } = await import('../checks.js');
156
+ const ctx = makeCtx({ config: null });
157
+ const result = await checkAppExists(ctx);
158
+ expect(result.status).toBe('skip');
159
+ });
160
+ it('passes when app found on server', async () => {
161
+ const { checkAppExists } = await import('../checks.js');
162
+ const mockClient = {
163
+ getApp: vi.fn().mockResolvedValue({ id: 'app-1', name: 'Test' }),
164
+ };
165
+ const ctx = makeCtx({ config: makeConfig(), client: mockClient });
166
+ const result = await checkAppExists(ctx);
167
+ expect(result.status).toBe('pass');
168
+ });
169
+ it('fails when app not found', async () => {
170
+ const { checkAppExists } = await import('../checks.js');
171
+ const mockClient = {
172
+ getApp: vi.fn().mockRejectedValue(new Error('404')),
173
+ };
174
+ const ctx = makeCtx({ config: makeConfig(), client: mockClient });
175
+ const result = await checkAppExists(ctx);
176
+ expect(result.status).toBe('fail');
177
+ expect(result.message).toContain('not found');
178
+ });
179
+ });
180
+ // ── 7. Git Remote ────────────────────────────────────────────────────
181
+ describe('checkGitRemote', () => {
182
+ it('skips when no project config', async () => {
183
+ const { checkGitRemote } = await import('../checks.js');
184
+ const ctx = makeCtx({ config: null });
185
+ const result = await checkGitRemote(ctx);
186
+ expect(result.status).toBe('skip');
187
+ });
188
+ it('fails when not a git repo', async () => {
189
+ const { checkGitRemote } = await import('../checks.js');
190
+ mockedExistsSync.mockReturnValue(false);
191
+ const ctx = makeCtx({ config: makeConfig() });
192
+ const result = await checkGitRemote(ctx);
193
+ expect(result.status).toBe('fail');
194
+ expect(result.message).toContain('not a git repository');
195
+ });
196
+ it('passes when remote configured correctly', async () => {
197
+ const { checkGitRemote } = await import('../checks.js');
198
+ mockedExistsSync.mockReturnValue(true);
199
+ mockedExecFileSync.mockReturnValue(Buffer.from('https://runwork.ai/git/ws-1/app-1'));
200
+ const ctx = makeCtx({ config: makeConfig() });
201
+ const result = await checkGitRemote(ctx);
202
+ expect(result.status).toBe('pass');
203
+ });
204
+ it('fails when remote not configured', async () => {
205
+ const { checkGitRemote } = await import('../checks.js');
206
+ mockedExistsSync.mockReturnValue(true);
207
+ mockedExecFileSync.mockImplementation(() => { throw new Error('No such remote'); });
208
+ const ctx = makeCtx({ config: makeConfig() });
209
+ const result = await checkGitRemote(ctx);
210
+ expect(result.status).toBe('fail');
211
+ expect(result.fix).toContain('git remote add');
212
+ });
213
+ });
214
+ // ── 8. Agent Setup ───────────────────────────────────────────────────
215
+ describe('checkAgentSetup', () => {
216
+ it('fails when setup never run', async () => {
217
+ const { checkAgentSetup } = await import('../checks.js');
218
+ mockedExistsSync.mockReturnValue(false);
219
+ const result = await checkAgentSetup();
220
+ expect(result.status).toBe('fail');
221
+ expect(result.fix).toContain('runwork setup');
222
+ });
223
+ });
@@ -0,0 +1,20 @@
1
+ import { ApiClient } from '../api/client.js';
2
+ import type { RunworkConfig, Credentials } from '../types.js';
3
+ import type { CheckResult } from './types.js';
4
+ export interface DoctorContext {
5
+ credentials: Credentials | null;
6
+ client: ApiClient | null;
7
+ config: RunworkConfig | null;
8
+ cwd: string;
9
+ }
10
+ export declare function buildContext(): DoctorContext;
11
+ export declare function checkSystemDeps(): Promise<CheckResult>;
12
+ export declare function checkCliVersion(): Promise<CheckResult>;
13
+ export declare function checkAuthAndNetwork(ctx: DoctorContext): Promise<{
14
+ auth: CheckResult;
15
+ network: CheckResult;
16
+ }>;
17
+ export declare function checkProjectConfig(ctx: DoctorContext): Promise<CheckResult>;
18
+ export declare function checkAppExists(ctx: DoctorContext): Promise<CheckResult>;
19
+ export declare function checkGitRemote(ctx: DoctorContext): Promise<CheckResult>;
20
+ export declare function checkAgentSetup(): Promise<CheckResult>;
@@ -0,0 +1,392 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ import { VERSION } from '../generated/version.js';
6
+ import { getCredentials } from '../auth/store.js';
7
+ import { ApiClient } from '../api/client.js';
8
+ import { detectAgents, getAdapterBySlug } from '../agents/detect.js';
9
+ export function buildContext() {
10
+ const credentials = getCredentials();
11
+ const client = credentials ? new ApiClient(credentials) : null;
12
+ let config = null;
13
+ const configPath = join(process.cwd(), '.runwork.json');
14
+ if (existsSync(configPath)) {
15
+ try {
16
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
17
+ }
18
+ catch {
19
+ // will be reported as a failure in checkProjectConfig
20
+ }
21
+ }
22
+ return { credentials, client, config, cwd: process.cwd() };
23
+ }
24
+ // ── 1. System dependencies ───────────────────────────────────────────
25
+ export async function checkSystemDeps() {
26
+ try {
27
+ const output = execFileSync('git', ['--version'], { stdio: 'pipe' }).toString().trim();
28
+ const match = output.match(/(\d+\.\d+(?:\.\d+)?)/);
29
+ const version = match ? match[1] : 'unknown';
30
+ if (match) {
31
+ const [major, minor] = version.split('.').map(Number);
32
+ if (major < 2 || (major === 2 && minor < 20)) {
33
+ return { name: 'system-deps', status: 'warn', message: `git ${version} (old -- consider upgrading)` };
34
+ }
35
+ }
36
+ return { name: 'system-deps', status: 'pass', message: `git ${version}` };
37
+ }
38
+ catch {
39
+ return {
40
+ name: 'system-deps',
41
+ status: 'fail',
42
+ message: 'git not found in PATH',
43
+ fix: 'Install git: https://git-scm.com/downloads',
44
+ };
45
+ }
46
+ }
47
+ // ── 2. CLI version ───────────────────────────────────────────────────
48
+ export async function checkCliVersion() {
49
+ try {
50
+ const response = await fetch('https://api.github.com/repos/runwork-ai/cli/releases/latest');
51
+ if (!response.ok) {
52
+ return { name: 'cli-version', status: 'skip', message: `${VERSION} (could not check for updates)` };
53
+ }
54
+ const data = await response.json();
55
+ const latest = data.tag_name?.replace(/^v/, '') || null;
56
+ if (!latest) {
57
+ return { name: 'cli-version', status: 'skip', message: `${VERSION} (could not parse latest)` };
58
+ }
59
+ if (latest === VERSION) {
60
+ return { name: 'cli-version', status: 'pass', message: `${VERSION} (latest)` };
61
+ }
62
+ return {
63
+ name: 'cli-version',
64
+ status: 'warn',
65
+ message: `${VERSION} (${latest} available)`,
66
+ fix: 'runwork upgrade',
67
+ };
68
+ }
69
+ catch {
70
+ return { name: 'cli-version', status: 'skip', message: `${VERSION} (could not check for updates)` };
71
+ }
72
+ }
73
+ // ── 3 & 4. Authentication + Network (combined) ──────────────────────
74
+ export async function checkAuthAndNetwork(ctx) {
75
+ if (!ctx.credentials) {
76
+ return {
77
+ auth: { name: 'auth', status: 'fail', message: 'not logged in', fix: 'runwork login' },
78
+ network: { name: 'network', status: 'skip', message: 'skipped (no credentials)' },
79
+ };
80
+ }
81
+ const start = Date.now();
82
+ try {
83
+ const workspaces = await ctx.client.listWorkspaces();
84
+ const elapsed = Date.now() - start;
85
+ const email = ctx.credentials.email || 'authenticated';
86
+ const wsCount = workspaces.length;
87
+ const auth = {
88
+ name: 'auth',
89
+ status: 'pass',
90
+ message: `${email} (${wsCount} workspace${wsCount !== 1 ? 's' : ''})`,
91
+ };
92
+ const network = elapsed > 3000
93
+ ? { name: 'network', status: 'warn', message: `API reachable (${elapsed}ms -- slow)` }
94
+ : { name: 'network', status: 'pass', message: `API reachable (${elapsed}ms)` };
95
+ return { auth, network };
96
+ }
97
+ catch (err) {
98
+ const elapsed = Date.now() - start;
99
+ const message = err instanceof Error ? err.message : String(err);
100
+ // Distinguish auth error from network error
101
+ if (message.includes('401') || message.includes('403') || message.includes('Unauthorized')) {
102
+ return {
103
+ auth: { name: 'auth', status: 'fail', message: 'token rejected by server', fix: 'runwork login' },
104
+ network: { name: 'network', status: 'pass', message: `API reachable (${elapsed}ms)` },
105
+ };
106
+ }
107
+ return {
108
+ auth: { name: 'auth', status: 'skip', message: 'could not verify (network error)' },
109
+ network: { name: 'network', status: 'fail', message: `API unreachable: ${message}` },
110
+ };
111
+ }
112
+ }
113
+ // ── 5. Project config ────────────────────────────────────────────────
114
+ export async function checkProjectConfig(ctx) {
115
+ const configPath = join(ctx.cwd, '.runwork.json');
116
+ if (!existsSync(configPath)) {
117
+ // Outside project dir -- check default workspace
118
+ if (!ctx.credentials) {
119
+ return { name: 'project-config', status: 'skip', message: 'no project (not logged in)' };
120
+ }
121
+ if (ctx.credentials.defaultWorkspaceId) {
122
+ const wsName = ctx.credentials.defaultWorkspaceName || ctx.credentials.defaultWorkspaceId;
123
+ return { name: 'project-config', status: 'pass', message: `no project dir (default workspace: ${wsName})` };
124
+ }
125
+ return {
126
+ name: 'project-config',
127
+ status: 'warn',
128
+ message: 'no project dir, no default workspace',
129
+ fix: 'runwork init <name> or runwork clone <app>',
130
+ };
131
+ }
132
+ // Inside project dir
133
+ if (!ctx.config) {
134
+ return {
135
+ name: 'project-config',
136
+ status: 'fail',
137
+ message: '.runwork.json is malformed',
138
+ fix: 'delete and re-run runwork init or runwork clone',
139
+ };
140
+ }
141
+ if (!ctx.config.workspaceId || !ctx.config.appId) {
142
+ return {
143
+ name: 'project-config',
144
+ status: 'fail',
145
+ message: '.runwork.json missing workspaceId or appId',
146
+ };
147
+ }
148
+ const wsName = ctx.config.workspaceName || ctx.config.workspaceId;
149
+ const appName = ctx.config.appName || ctx.config.appId;
150
+ return { name: 'project-config', status: 'pass', message: `${appName} (ws: ${wsName})` };
151
+ }
152
+ // ── 6. App exists on server ──────────────────────────────────────────
153
+ export async function checkAppExists(ctx) {
154
+ if (!ctx.config?.appId || !ctx.client) {
155
+ return { name: 'app-exists', status: 'skip', message: 'skipped (no project or no auth)' };
156
+ }
157
+ try {
158
+ const app = await ctx.client.getApp(ctx.config.appId);
159
+ if (app) {
160
+ return { name: 'app-exists', status: 'pass', message: 'confirmed on server' };
161
+ }
162
+ return {
163
+ name: 'app-exists',
164
+ status: 'fail',
165
+ message: `app ${ctx.config.appId} not found on server`,
166
+ fix: 'app may have been deleted -- re-create with runwork init',
167
+ };
168
+ }
169
+ catch {
170
+ return {
171
+ name: 'app-exists',
172
+ status: 'fail',
173
+ message: `app ${ctx.config.appId} not found on server`,
174
+ fix: 'app may have been deleted -- re-create with runwork init',
175
+ };
176
+ }
177
+ }
178
+ // ── 7. Git remote ────────────────────────────────────────────────────
179
+ export async function checkGitRemote(ctx) {
180
+ if (!ctx.config) {
181
+ return { name: 'git-remote', status: 'skip', message: 'skipped (no project)' };
182
+ }
183
+ // Check if we're in a git repo
184
+ if (!existsSync(join(ctx.cwd, '.git'))) {
185
+ return {
186
+ name: 'git-remote',
187
+ status: 'fail',
188
+ message: 'not a git repository',
189
+ fix: 'git init',
190
+ };
191
+ }
192
+ try {
193
+ const remoteUrl = execFileSync('git', ['remote', 'get-url', 'runwork'], {
194
+ cwd: ctx.cwd,
195
+ stdio: 'pipe',
196
+ }).toString().trim();
197
+ // Check if URL looks right (should contain workspace and app IDs)
198
+ if (ctx.config.workspaceId && !remoteUrl.includes(ctx.config.workspaceId)) {
199
+ return {
200
+ name: 'git-remote',
201
+ status: 'warn',
202
+ message: `remote "runwork" URL may be stale: ${remoteUrl}`,
203
+ };
204
+ }
205
+ return { name: 'git-remote', status: 'pass', message: `remote configured` };
206
+ }
207
+ catch {
208
+ const baseUrl = ctx.credentials?.baseUrl || 'https://runwork.ai';
209
+ const expectedUrl = `${baseUrl}/git/${ctx.config.workspaceId}/${ctx.config.appId}`;
210
+ return {
211
+ name: 'git-remote',
212
+ status: 'fail',
213
+ message: 'remote "runwork" not configured',
214
+ fix: `git remote add runwork ${expectedUrl}`,
215
+ };
216
+ }
217
+ }
218
+ // ── 8. Agent setup ───────────────────────────────────────────────────
219
+ function loadSetupState() {
220
+ // Try project-level first, then user-level
221
+ const projectPath = join(process.cwd(), '.runwork', 'setup.json');
222
+ const userPath = join(homedir(), '.runwork', 'setup.json');
223
+ for (const p of [projectPath, userPath]) {
224
+ if (existsSync(p)) {
225
+ try {
226
+ return JSON.parse(readFileSync(p, 'utf-8'));
227
+ }
228
+ catch {
229
+ continue;
230
+ }
231
+ }
232
+ }
233
+ return null;
234
+ }
235
+ export async function checkAgentSetup() {
236
+ const state = loadSetupState();
237
+ if (!state) {
238
+ return {
239
+ name: 'agent-setup',
240
+ status: 'fail',
241
+ message: 'setup has never been run',
242
+ fix: 'runwork setup',
243
+ };
244
+ }
245
+ const details = [];
246
+ let worstStatus = 'pass';
247
+ const upgrade = (s) => {
248
+ if (s === 'fail' || (s === 'warn' && worstStatus === 'pass'))
249
+ worstStatus = s;
250
+ };
251
+ // Sub-check: sync freshness
252
+ const syncAge = Date.now() - new Date(state.lastSyncAt).getTime();
253
+ const syncDays = Math.floor(syncAge / (1000 * 60 * 60 * 24));
254
+ if (syncDays > 30) {
255
+ details.push(`last synced ${syncDays} days ago (stale)`);
256
+ upgrade('fail');
257
+ }
258
+ else if (syncDays > 7) {
259
+ details.push(`last synced ${syncDays} days ago`);
260
+ upgrade('warn');
261
+ }
262
+ else {
263
+ details.push(`last synced ${syncDays === 0 ? 'today' : `${syncDays}d ago`}`);
264
+ }
265
+ // Sub-check: configured agents still detected
266
+ const detected = await detectAgents();
267
+ const detectedSlugs = new Set(detected.map(a => a.slug));
268
+ const missingAgents = state.configuredAgents.filter(s => !detectedSlugs.has(s));
269
+ if (missingAgents.length > 0) {
270
+ details.push(`${missingAgents.length} agent(s) no longer detected: ${missingAgents.join(', ')}`);
271
+ upgrade('warn');
272
+ }
273
+ else {
274
+ details.push(`${state.configuredAgents.length} agent(s) configured`);
275
+ }
276
+ // Sub-check: MCP servers present in agent configs
277
+ // We check the first configured agent that supports MCP
278
+ let mcpChecked = false;
279
+ for (const slug of state.configuredAgents) {
280
+ const adapter = getAdapterBySlug(slug);
281
+ if (!adapter || !adapter.supportsMcpScope('user'))
282
+ continue;
283
+ // Check if MCP config file exists and contains our entries
284
+ const mcpConfigPath = getMcpConfigPath(slug, 'user');
285
+ if (mcpConfigPath && existsSync(mcpConfigPath)) {
286
+ try {
287
+ const content = readFileSync(mcpConfigPath, 'utf-8');
288
+ const missingMcp = state.mcpServers.filter(name => !content.includes(name));
289
+ if (missingMcp.length > 0) {
290
+ details.push(`${missingMcp.length} MCP server(s) missing from ${slug} config`);
291
+ upgrade('warn');
292
+ }
293
+ else {
294
+ details.push(`${state.mcpServers.length} MCP server(s) configured`);
295
+ }
296
+ mcpChecked = true;
297
+ break;
298
+ }
299
+ catch {
300
+ // can't read config, skip
301
+ }
302
+ }
303
+ }
304
+ if (!mcpChecked && state.mcpServers.length > 0) {
305
+ details.push('could not verify MCP servers');
306
+ upgrade('warn');
307
+ }
308
+ // Sub-check: skills present
309
+ let skillsChecked = false;
310
+ for (const slug of state.configuredAgents) {
311
+ const skillsDir = getSkillsDir(slug, 'user');
312
+ if (!skillsDir)
313
+ continue;
314
+ const missingSkills = state.skills.filter(name => {
315
+ const skillPath = join(skillsDir, name, 'SKILL.md');
316
+ return !existsSync(skillPath);
317
+ });
318
+ if (missingSkills.length > 0) {
319
+ details.push(`${missingSkills.length} skill(s) missing from ${slug}`);
320
+ upgrade('warn');
321
+ }
322
+ else if (state.skills.length > 0) {
323
+ details.push(`${state.skills.length} skill(s) installed`);
324
+ }
325
+ skillsChecked = true;
326
+ break;
327
+ }
328
+ if (!skillsChecked && state.skills.length > 0) {
329
+ details.push('could not verify skills');
330
+ upgrade('warn');
331
+ }
332
+ // Build summary message
333
+ const summary = worstStatus === 'pass'
334
+ ? details[0]
335
+ : details.filter(d => !d.startsWith(`${state.configuredAgents.length} agent`) &&
336
+ !d.startsWith(`${state.mcpServers.length} MCP`) &&
337
+ !d.startsWith(`${state.skills.length} skill`)).join('; ') || details[0];
338
+ return {
339
+ name: 'agent-setup',
340
+ status: worstStatus,
341
+ message: summary,
342
+ details,
343
+ ...(worstStatus !== 'pass' ? { fix: 'runwork sync' } : {}),
344
+ };
345
+ }
346
+ // ── Helpers for agent config paths ───────────────────────────────────
347
+ function getMcpConfigPath(slug, scope) {
348
+ const home = homedir();
349
+ switch (slug) {
350
+ case 'claude-code':
351
+ return scope === 'project'
352
+ ? join(process.cwd(), '.mcp.json')
353
+ : join(home, '.claude', 'settings.json');
354
+ case 'cursor':
355
+ return scope === 'project'
356
+ ? join(process.cwd(), '.cursor', 'mcp.json')
357
+ : join(home, '.cursor', 'mcp.json');
358
+ case 'windsurf':
359
+ return scope === 'project'
360
+ ? join(process.cwd(), '.windsurf', 'mcp.json')
361
+ : join(home, '.windsurf', 'mcp.json');
362
+ case 'codex':
363
+ return scope === 'user'
364
+ ? join(home, '.codex', 'mcp.json')
365
+ : null;
366
+ case 'gemini':
367
+ return scope === 'user'
368
+ ? join(home, '.gemini', 'settings.json')
369
+ : null;
370
+ default:
371
+ return null;
372
+ }
373
+ }
374
+ function getSkillsDir(slug, scope) {
375
+ const home = homedir();
376
+ switch (slug) {
377
+ case 'claude-code':
378
+ return scope === 'project'
379
+ ? join(process.cwd(), '.claude', 'skills')
380
+ : join(home, '.claude', 'skills');
381
+ case 'codex':
382
+ return scope === 'project'
383
+ ? join(process.cwd(), '.codex', 'skills')
384
+ : join(home, '.codex', 'skills');
385
+ case 'gemini':
386
+ return scope === 'project'
387
+ ? join(process.cwd(), '.gemini', 'skills')
388
+ : join(home, '.gemini', 'skills');
389
+ default:
390
+ return null;
391
+ }
392
+ }
@@ -0,0 +1,2 @@
1
+ import type { DoctorReport } from './types.js';
2
+ export declare function runAllChecks(): Promise<DoctorReport>;
@@ -0,0 +1,26 @@
1
+ import { buildContext, checkSystemDeps, checkCliVersion, checkAuthAndNetwork, checkProjectConfig, checkAppExists, checkGitRemote, checkAgentSetup, } from './checks.js';
2
+ export async function runAllChecks() {
3
+ const ctx = buildContext();
4
+ const checks = [];
5
+ // 1. System deps
6
+ checks.push(await checkSystemDeps());
7
+ // 2. CLI version
8
+ checks.push(await checkCliVersion());
9
+ // 3 & 4. Auth + Network (combined call)
10
+ const { auth, network } = await checkAuthAndNetwork(ctx);
11
+ checks.push(auth);
12
+ checks.push(network);
13
+ // 5. Project config
14
+ checks.push(await checkProjectConfig(ctx));
15
+ // 6. App exists
16
+ checks.push(await checkAppExists(ctx));
17
+ // 7. Git remote
18
+ checks.push(await checkGitRemote(ctx));
19
+ // 8. Agent setup
20
+ checks.push(await checkAgentSetup());
21
+ // Overall status: worst of all checks
22
+ const hasFailure = checks.some(c => c.status === 'fail');
23
+ const hasWarning = checks.some(c => c.status === 'warn');
24
+ const status = hasFailure ? 'fail' : hasWarning ? 'warn' : 'pass';
25
+ return { status, checks };
26
+ }
@@ -0,0 +1,12 @@
1
+ export type CheckStatus = 'pass' | 'warn' | 'fail' | 'skip';
2
+ export interface CheckResult {
3
+ name: string;
4
+ status: CheckStatus;
5
+ message: string;
6
+ fix?: string;
7
+ details?: string[];
8
+ }
9
+ export interface DoctorReport {
10
+ status: CheckStatus;
11
+ checks: CheckResult[];
12
+ }
@@ -0,0 +1 @@
1
+ export {};