runwork 0.6.2 → 0.8.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.
Files changed (80) hide show
  1. package/dist/__tests__/install-scripts.test.d.ts +1 -0
  2. package/dist/__tests__/install-scripts.test.js +247 -0
  3. package/dist/agents/__tests__/claude-code-plugin.test.d.ts +1 -0
  4. package/dist/agents/__tests__/claude-code-plugin.test.js +47 -0
  5. package/dist/agents/__tests__/claude-code-stats.test.js +143 -78
  6. package/dist/agents/__tests__/claude-desktop-plugin-tree.test.d.ts +1 -0
  7. package/dist/agents/__tests__/claude-desktop-plugin-tree.test.js +161 -0
  8. package/dist/agents/__tests__/claude-desktop-plugin.test.d.ts +1 -0
  9. package/dist/agents/__tests__/claude-desktop-plugin.test.js +117 -0
  10. package/dist/agents/__tests__/claude-desktop-rpm.test.d.ts +1 -0
  11. package/dist/agents/__tests__/claude-desktop-rpm.test.js +126 -0
  12. package/dist/agents/__tests__/codex-instructions.test.d.ts +1 -0
  13. package/dist/agents/__tests__/codex-instructions.test.js +57 -0
  14. package/dist/agents/__tests__/codex-stats.test.js +9 -0
  15. package/dist/agents/__tests__/graceful-degradation.test.js +3 -3
  16. package/dist/agents/__tests__/intro-skill.test.js +11 -2
  17. package/dist/agents/__tests__/skill-slug.test.d.ts +1 -0
  18. package/dist/agents/__tests__/skill-slug.test.js +77 -0
  19. package/dist/agents/claude-code.d.ts +3 -1
  20. package/dist/agents/claude-code.js +233 -58
  21. package/dist/agents/claude-desktop-plugin-tree.d.ts +44 -0
  22. package/dist/agents/claude-desktop-plugin-tree.js +92 -0
  23. package/dist/agents/claude-desktop.d.ts +20 -1
  24. package/dist/agents/claude-desktop.js +238 -61
  25. package/dist/agents/cline.d.ts +3 -1
  26. package/dist/agents/cline.js +44 -2
  27. package/dist/agents/codex.d.ts +3 -1
  28. package/dist/agents/codex.js +85 -14
  29. package/dist/agents/cursor.d.ts +12 -1
  30. package/dist/agents/cursor.js +98 -25
  31. package/dist/agents/gemini.d.ts +3 -1
  32. package/dist/agents/gemini.js +42 -4
  33. package/dist/agents/generic-adapter.d.ts +3 -1
  34. package/dist/agents/generic-adapter.js +59 -4
  35. package/dist/agents/intro-skill.js +77 -31
  36. package/dist/agents/types.d.ts +42 -2
  37. package/dist/agents/types.js +61 -6
  38. package/dist/agents/utils/instruction-hint.d.ts +10 -0
  39. package/dist/agents/utils/instruction-hint.js +46 -0
  40. package/dist/agents/utils/json-config.d.ts +6 -1
  41. package/dist/agents/utils/json-config.js +31 -4
  42. package/dist/agents/windsurf.d.ts +3 -1
  43. package/dist/agents/windsurf.js +37 -2
  44. package/dist/commands/__tests__/sync-telemetry.test.d.ts +1 -0
  45. package/dist/commands/__tests__/sync-telemetry.test.js +281 -0
  46. package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
  47. package/dist/commands/__tests__/upgrade.test.js +174 -0
  48. package/dist/commands/build-plugin.d.ts +2 -0
  49. package/dist/commands/build-plugin.js +135 -0
  50. package/dist/commands/init.d.ts +8 -2
  51. package/dist/commands/init.js +19 -7
  52. package/dist/commands/sync-telemetry.d.ts +33 -0
  53. package/dist/commands/sync-telemetry.js +186 -0
  54. package/dist/commands/sync.d.ts +1 -0
  55. package/dist/commands/sync.js +84 -118
  56. package/dist/commands/uninstall.d.ts +2 -0
  57. package/dist/commands/uninstall.js +145 -0
  58. package/dist/commands/upgrade.d.ts +12 -0
  59. package/dist/commands/upgrade.js +67 -16
  60. package/dist/generated/bundled-types.js +36 -36
  61. package/dist/generated/version.d.ts +1 -1
  62. package/dist/generated/version.js +1 -1
  63. package/dist/health/__tests__/cli-distribution-checks.test.d.ts +1 -0
  64. package/dist/health/__tests__/cli-distribution-checks.test.js +199 -0
  65. package/dist/health/checks.d.ts +2 -0
  66. package/dist/health/checks.js +131 -18
  67. package/dist/health/runner.js +6 -2
  68. package/dist/index.js +4 -0
  69. package/dist/sync/executor.d.ts +2 -0
  70. package/dist/sync/executor.js +8 -4
  71. package/dist/types.d.ts +2 -0
  72. package/dist/ui/banner.js +15 -6
  73. package/dist/utils/__tests__/sqlite.test.js +84 -72
  74. package/dist/utils/sqlite-adapter.d.ts +48 -0
  75. package/dist/utils/sqlite-adapter.js +147 -0
  76. package/dist/utils/sqlite.d.ts +12 -5
  77. package/dist/utils/sqlite.js +12 -22
  78. package/dist/utils/zip.d.ts +6 -0
  79. package/dist/utils/zip.js +28 -3
  80. package/package.json +2 -4
@@ -5,7 +5,8 @@ import { homedir } from 'os';
5
5
  import { requireAuth } from '../auth/store.js';
6
6
  import { ApiClient } from '../api/client.js';
7
7
  import { getAdapterBySlug } from '../agents/detect.js';
8
- import { RUNWORK_MCP_PREFIX } from '../agents/types.js';
8
+ import { RUNWORK_MCP_PREFIX, RUNWORK_WORKSPACE_MCP_NAME } from '../agents/types.js';
9
+ import { collectTelemetryEvents, printTelemetryVerbose, summarizeTelemetryForDryRun, } from './sync-telemetry.js';
9
10
  import { generateIntroSkill, generateInstructionHint, buildAppSkillDescription } from '../agents/intro-skill.js';
10
11
  import { computeSyncPlan } from '../sync/change-detect.js';
11
12
  import { printSyncSummary, resolveConflict, resolveConflictNonInteractive } from '../sync/conflict-ui.js';
@@ -119,7 +120,7 @@ export async function syncFromState(state, statePath, credentials, opts) {
119
120
  description: `Runwork MCP server: ${s.name}. Provides tools for interacting with this workspace resource.`,
120
121
  }));
121
122
  mcpEntries.push({
122
- name: `${RUNWORK_MCP_PREFIX}Workspace Tools`,
123
+ name: RUNWORK_WORKSPACE_MCP_NAME,
123
124
  url: `${baseUrl}/api/workspaces/${state.workspaceId}/mcp`,
124
125
  transport: 'streamable-http',
125
126
  headers: { Authorization: `Bearer ${credentials.apiKey}` },
@@ -158,8 +159,35 @@ export async function syncFromState(state, statePath, credentials, opts) {
158
159
  else {
159
160
  console.log(' All skills up to date.');
160
161
  }
161
- if (opts.dryRun)
162
+ // Build adapters early so telemetry collection works in both dry-run and real paths
163
+ const adapters = [];
164
+ for (const slug of state.configuredAgents) {
165
+ const adapter = getAdapterBySlug(slug);
166
+ if (adapter)
167
+ adapters.push(adapter);
168
+ }
169
+ if (opts.dryRun) {
170
+ try {
171
+ const telemetry = await collectTelemetryEvents({
172
+ adapters,
173
+ state,
174
+ syncedSkillsCount: remoteSkills.length,
175
+ syncedMcpServersCount: mcpEntries.length,
176
+ teamInstructionsApplied: false,
177
+ agentConfigsApplied: 0,
178
+ });
179
+ if (opts.verbose) {
180
+ printTelemetryVerbose(telemetry);
181
+ }
182
+ else {
183
+ console.log(`\n ${summarizeTelemetryForDryRun(telemetry)}`);
184
+ }
185
+ }
186
+ catch {
187
+ // Telemetry preview is best-effort
188
+ }
162
189
  return;
190
+ }
163
191
  // Resolve conflicts
164
192
  const resolvedConflicts = new Map();
165
193
  for (const conflict of plan.conflicts) {
@@ -176,13 +204,6 @@ export async function syncFromState(state, statePath, credentials, opts) {
176
204
  }
177
205
  }
178
206
  }
179
- // Build adapters
180
- const adapters = [];
181
- for (const slug of state.configuredAgents) {
182
- const adapter = getAdapterBySlug(slug);
183
- if (adapter)
184
- adapters.push(adapter);
185
- }
186
207
  const scopes = state.scope === 'both' ? ['project', 'user'] : [state.scope];
187
208
  if (adapters.length > 0) {
188
209
  console.log(` Syncing to: ${adapters.map(a => a.name).join(', ')}`);
@@ -193,6 +214,7 @@ export async function syncFromState(state, statePath, credentials, opts) {
193
214
  workspaceId: state.workspaceId,
194
215
  adapters,
195
216
  scopes,
217
+ hasMcp: mcpEntries.length > 0,
196
218
  });
197
219
  // Write intro skill, MCP configs, instruction hints (pull-only)
198
220
  const introSkill = generateIntroSkill({
@@ -230,9 +252,16 @@ export async function syncFromState(state, statePath, credentials, opts) {
230
252
  for (const adapter of adapters) {
231
253
  for (const scope of scopes) {
232
254
  try {
233
- // Write skills: for project scope, filter to only the current app's skill
255
+ // Write skills: for project scope, filter to only the current app's skill.
256
+ // When MCP servers are configured for this adapter, skip app-sourced skills
257
+ // since the MCP server already exposes them as skill_* tools. Writing both
258
+ // causes duplicate entries in the agent's skill listing.
234
259
  if (adapter.supportsSkills()) {
260
+ const skipAppSkills = adapter.mcpProvidesSkills && mcpEntries.length > 0;
235
261
  const scopeSkills = remoteSkills.filter(s => {
262
+ // App-sourced skills are already available via MCP skill_* tools
263
+ if (skipAppSkills && s.source === 'app')
264
+ return false;
236
265
  if (scope === 'project' && s.source === 'app' && projectAppSkillFilter) {
237
266
  const filename = s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
238
267
  return filename === projectAppSkillFilter;
@@ -243,7 +272,9 @@ export async function syncFromState(state, statePath, credentials, opts) {
243
272
  name: s.name,
244
273
  filename: s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
245
274
  content: s.content,
246
- description: buildAppSkillDescription(s.name, registries) || `${s.name} skill`,
275
+ description: s.source === 'app'
276
+ ? (buildAppSkillDescription(s.name, registries) || `${s.name} - Runwork workspace application`)
277
+ : `${s.name} - Runwork workspace skill`,
247
278
  }))];
248
279
  await adapter.writeSkills(allSkillFiles, scope);
249
280
  }
@@ -316,10 +347,33 @@ export async function syncFromState(state, statePath, credentials, opts) {
316
347
  catch {
317
348
  // Team config pull is non-fatal
318
349
  }
350
+ // Ensure sandbox network access for agents that run in sandboxed environments (e.g. Cursor)
351
+ const networkDomains = ['runwork.ai', '*.runwork.ai'];
352
+ try {
353
+ const baseHost = new URL(baseUrl).hostname;
354
+ if (baseHost !== 'runwork.ai' && !baseHost.endsWith('.runwork.ai')) {
355
+ networkDomains.push(baseHost, `*.${baseHost}`);
356
+ }
357
+ }
358
+ catch { /* use defaults */ }
359
+ for (const adapter of adapters) {
360
+ if (!adapter.writeAgentConfig)
361
+ continue;
362
+ try {
363
+ await adapter.writeAgentConfig({ networkAllowlist: networkDomains }, 'user');
364
+ }
365
+ catch {
366
+ // Best-effort
367
+ }
368
+ }
319
369
  // Update state
320
370
  state.lastSyncAt = new Date().toISOString();
321
371
  state.mcpServers = mcpEntries.map(e => e.name);
322
372
  state.skills = remoteSkills.map(s => s.name);
373
+ state.skillFilenames = [
374
+ 'runwork', // intro skill is always synced
375
+ ...remoteSkills.map(s => s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')),
376
+ ];
323
377
  // Merge hashes: keep existing for skips, update for everything else
324
378
  const mergedHashes = { ...(state.skillHashes || {}) };
325
379
  for (const [name, hash] of Object.entries(newHashes)) {
@@ -333,112 +387,22 @@ export async function syncFromState(state, statePath, credentials, opts) {
333
387
  writeFileSync(statePath, JSON.stringify(state, null, 2));
334
388
  // Report telemetry (non-fatal)
335
389
  try {
336
- const now = new Date().toISOString();
337
- const previouslyReported = new Set(state.reportedAgentSlugs ?? []);
338
- const telemetryEvents = [];
339
- // Only report newly detected agents (not already reported in previous syncs)
340
- const newAgents = [];
341
- for (const slug of state.configuredAgents) {
342
- if (!previouslyReported.has(slug)) {
343
- telemetryEvents.push({
344
- eventType: 'local_agent.detected',
345
- agentSlug: slug,
346
- metadata: { installed: true },
347
- timestamp: now,
348
- });
349
- newAgents.push(slug);
350
- }
351
- }
352
- // Collect passive usage stats from each agent
353
- for (const adapter of adapters) {
354
- try {
355
- const stats = await adapter.readUsageStats?.(state.lastSyncAt ?? null);
356
- if (stats && stats.hasNewActivity) {
357
- telemetryEvents.push({
358
- eventType: 'local_agent.usage',
359
- agentSlug: adapter.slug,
360
- metadata: {
361
- sessionCount: stats.sessionCount,
362
- messageCount: stats.messageCount,
363
- toolCallCount: stats.toolCallCount,
364
- tokensUsed: stats.tokensUsed,
365
- aiLinesAdded: stats.aiLinesAdded,
366
- aiLinesRemoved: stats.aiLinesRemoved,
367
- lastActiveAt: stats.lastActiveAt,
368
- modelsUsed: stats.modelsUsed,
369
- mcpToolCount: stats.mcpToolCount,
370
- ...stats.extra,
371
- },
372
- timestamp: now,
373
- });
374
- }
375
- }
376
- catch {
377
- // Usage stats collection is best-effort
378
- }
379
- }
380
- // Collect skill usage stats
381
- for (const adapter of adapters) {
382
- try {
383
- const skillUsage = await adapter.readSkillUsage?.(state.lastSyncAt ?? null);
384
- if (skillUsage && skillUsage.length > 0) {
385
- for (const entry of skillUsage) {
386
- telemetryEvents.push({
387
- eventType: 'local_agent.skill_used',
388
- agentSlug: adapter.slug,
389
- metadata: {
390
- skillName: entry.skillName,
391
- count: entry.count,
392
- lastUsedAt: entry.lastUsedAt,
393
- },
394
- timestamp: now,
395
- });
396
- }
397
- }
398
- }
399
- catch {
400
- // Skill usage collection is best-effort
401
- }
402
- }
403
- // Report agent health once per day (version, platform)
404
- const lastHealthReport = state.lastHealthReportAt ? new Date(state.lastHealthReportAt) : null;
405
- const shouldReportHealth = !lastHealthReport ||
406
- (Date.now() - lastHealthReport.getTime()) > 24 * 60 * 60 * 1000;
407
- if (shouldReportHealth) {
408
- for (const adapter of adapters) {
409
- try {
410
- const version = await adapter.readVersion?.();
411
- telemetryEvents.push({
412
- eventType: 'local_agent.health',
413
- agentSlug: adapter.slug,
414
- metadata: {
415
- version,
416
- installed: true,
417
- platform: process.platform,
418
- arch: process.arch,
419
- },
420
- timestamp: now,
421
- });
422
- }
423
- catch {
424
- // Health reporting is best-effort
425
- }
426
- }
427
- state.lastHealthReportAt = now;
428
- }
429
- telemetryEvents.push({
430
- eventType: 'local_agent.sync_completed',
431
- metadata: {
432
- syncedSkills: remoteSkills.length,
433
- syncedMcpServers: mcpEntries.length,
434
- teamInstructionsApplied,
435
- agentConfigsApplied,
436
- },
437
- timestamp: now,
390
+ const telemetry = await collectTelemetryEvents({
391
+ adapters,
392
+ state,
393
+ syncedSkillsCount: remoteSkills.length,
394
+ syncedMcpServersCount: mcpEntries.length,
395
+ teamInstructionsApplied,
396
+ agentConfigsApplied,
438
397
  });
439
- await client.reportTelemetry(state.workspaceId, telemetryEvents);
440
- // Track which agents have been reported so we don't spam on future syncs
441
- state.reportedAgentSlugs = [...new Set([...previouslyReported, ...newAgents])];
398
+ if (opts.verbose)
399
+ printTelemetryVerbose(telemetry);
400
+ await client.reportTelemetry(state.workspaceId, telemetry.events);
401
+ if (telemetry.healthReported)
402
+ state.lastHealthReportAt = new Date().toISOString();
403
+ state.reportedAgentSlugs = [
404
+ ...new Set([...(state.reportedAgentSlugs ?? []), ...telemetry.newlyReportedAgents]),
405
+ ];
442
406
  writeFileSync(statePath, JSON.stringify(state, null, 2));
443
407
  }
444
408
  catch {
@@ -447,7 +411,8 @@ export async function syncFromState(state, statePath, credentials, opts) {
447
411
  }
448
412
  export const syncCommand = new Command('sync')
449
413
  .description('Sync skills bidirectionally and refresh MCP configs from workspace')
450
- .option('--dry-run', 'Show what would change without doing anything')
414
+ .option('--dry-run', 'Show what would change without doing anything (includes telemetry preview)')
415
+ .option('-v, --verbose', 'Print per-adapter stats and the telemetry envelope')
451
416
  .option('--pull-only', 'Only pull remote changes, never push local edits')
452
417
  .option('--prefer <side>', 'Auto-resolve conflicts: "local" or "remote" (implies --yes)')
453
418
  .option('-y, --yes', 'Non-interactive mode (default: prefer remote for conflicts)')
@@ -458,6 +423,7 @@ export const syncCommand = new Command('sync')
458
423
  pullOnly: !!opts.pullOnly,
459
424
  yes: !!opts.yes || !!opts.prefer,
460
425
  prefer: opts.prefer,
426
+ verbose: !!opts.verbose,
461
427
  };
462
428
  const projectStatePath = join(process.cwd(), '.runwork', 'setup.json');
463
429
  const userStatePath = join(homedir(), '.runwork', 'setup.json');
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const uninstallCommand: Command;
@@ -0,0 +1,145 @@
1
+ import { Command } from 'commander';
2
+ import { existsSync, readFileSync, rmSync, unlinkSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ import { getAdapterBySlug } from '../agents/detect.js';
6
+ import { promptConfirm } from '../utils/prompt.js';
7
+ function loadSetupState(filePath) {
8
+ if (!existsSync(filePath))
9
+ return null;
10
+ try {
11
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export const uninstallCommand = new Command('uninstall')
18
+ .description('Remove all Runwork configuration from local agents (MCP servers, skills, instructions)')
19
+ .option('-y, --yes', 'Skip confirmation prompt')
20
+ .option('--keep-auth', 'Keep authentication credentials (only remove agent configs)')
21
+ .action(async (opts) => {
22
+ const projectStatePath = join(process.cwd(), '.runwork', 'setup.json');
23
+ const userStatePath = join(homedir(), '.runwork', 'setup.json');
24
+ const projectState = loadSetupState(projectStatePath);
25
+ const userState = loadSetupState(userStatePath);
26
+ if (!projectState && !userState) {
27
+ console.error('No setup state found. Nothing to uninstall.');
28
+ process.exit(0);
29
+ }
30
+ // Collect all configured agents across both scopes
31
+ const agentSlugs = new Set();
32
+ if (projectState) {
33
+ for (const slug of projectState.configuredAgents)
34
+ agentSlugs.add(slug);
35
+ }
36
+ if (userState) {
37
+ for (const slug of userState.configuredAgents)
38
+ agentSlugs.add(slug);
39
+ }
40
+ console.log('\nThis will remove all Runwork configuration from your local agents:\n');
41
+ if (agentSlugs.size > 0) {
42
+ console.log(' Agents to clean:');
43
+ for (const slug of agentSlugs) {
44
+ const adapter = getAdapterBySlug(slug);
45
+ console.log(` - ${adapter?.name ?? slug}`);
46
+ }
47
+ }
48
+ console.log('\n What will be removed:');
49
+ console.log(' - MCP server registrations');
50
+ console.log(' - Synced skills and instruction hints');
51
+ console.log(' - Team instructions');
52
+ console.log(' - Agent config overrides (model, permissions)');
53
+ console.log(' - Plugin registrations (Claude Code, Claude Desktop)');
54
+ if (opts.keepAuth) {
55
+ console.log(' - Setup state (keeping auth credentials)');
56
+ }
57
+ else {
58
+ console.log(' - Setup state and auth credentials (~/.runwork/)');
59
+ }
60
+ console.log('');
61
+ if (!opts.yes) {
62
+ const confirmed = await promptConfirm('Proceed with uninstall?');
63
+ if (!confirmed) {
64
+ console.log('Cancelled.');
65
+ return;
66
+ }
67
+ }
68
+ let cleanedAgents = 0;
69
+ let errors = 0;
70
+ // Process each setup state (project and/or user)
71
+ const entries = [];
72
+ if (projectState)
73
+ entries.push({ state: projectState, statePath: projectStatePath, label: 'project' });
74
+ if (userState)
75
+ entries.push({ state: userState, statePath: userStatePath, label: 'user' });
76
+ for (const { state, label } of entries) {
77
+ const scopes = state.scope === 'both'
78
+ ? ['project', 'user']
79
+ : [state.scope];
80
+ // Build manifest from sync state so adapters only remove what we synced
81
+ const manifest = {
82
+ skillFilenames: state.skillFilenames ?? state.skills.map(s => s.toLowerCase().replace(/[^a-z0-9]+/g, '-')),
83
+ mcpServerNames: state.mcpServers ?? [],
84
+ };
85
+ for (const slug of state.configuredAgents) {
86
+ const adapter = getAdapterBySlug(slug);
87
+ if (!adapter) {
88
+ console.warn(` [${slug}] Adapter not found, skipping.`);
89
+ continue;
90
+ }
91
+ for (const scope of scopes) {
92
+ try {
93
+ if (adapter.cleanup) {
94
+ await adapter.cleanup(scope, manifest);
95
+ console.log(` [${adapter.name}] Cleaned up (${scope})`);
96
+ cleanedAgents++;
97
+ }
98
+ else {
99
+ console.log(` [${adapter.name}] No cleanup handler, skipping (${scope})`);
100
+ }
101
+ }
102
+ catch (err) {
103
+ console.warn(` [${adapter.name}] Cleanup failed (${scope}): ${err instanceof Error ? err.message : err}`);
104
+ errors++;
105
+ }
106
+ }
107
+ }
108
+ // Remove setup state
109
+ const stateDir = label === 'project'
110
+ ? join(process.cwd(), '.runwork')
111
+ : join(homedir(), '.runwork');
112
+ if (opts.keepAuth && label === 'user') {
113
+ // Only remove setup.json, keep credentials and other files
114
+ const setupFile = join(stateDir, 'setup.json');
115
+ if (existsSync(setupFile)) {
116
+ try {
117
+ unlinkSync(setupFile);
118
+ console.log(` Removed ${setupFile} (kept credentials)`);
119
+ }
120
+ catch (err) {
121
+ console.warn(` Failed to remove ${setupFile}: ${err instanceof Error ? err.message : err}`);
122
+ errors++;
123
+ }
124
+ }
125
+ }
126
+ else if (existsSync(stateDir)) {
127
+ try {
128
+ rmSync(stateDir, { recursive: true, force: true });
129
+ console.log(` Removed ${stateDir}`);
130
+ }
131
+ catch (err) {
132
+ console.warn(` Failed to remove ${stateDir}: ${err instanceof Error ? err.message : err}`);
133
+ errors++;
134
+ }
135
+ }
136
+ }
137
+ console.log('');
138
+ if (errors > 0) {
139
+ console.log(`Uninstall completed with ${errors} warning${errors > 1 ? 's' : ''}. ${cleanedAgents} agent${cleanedAgents > 1 ? 's' : ''} cleaned.`);
140
+ }
141
+ else {
142
+ console.log(`Uninstall complete. ${cleanedAgents} agent${cleanedAgents > 1 ? 's' : ''} cleaned.`);
143
+ }
144
+ console.log('Run `runwork setup` to reconfigure at any time.');
145
+ });
@@ -1,2 +1,14 @@
1
1
  import { Command } from 'commander';
2
+ declare function fetchLatestVersion(): Promise<string | null>;
3
+ type InstallMethod = 'brew' | 'npm' | 'bun' | 'pnpm' | 'yarn' | 'volta' | 'binary';
4
+ declare function detectInstallMethod(): InstallMethod;
2
5
  export declare const upgradeCommand: Command;
6
+ export declare const __internal: {
7
+ BASE_URL: string;
8
+ LATEST_JSON_URL: string;
9
+ INSTALL_SH_URL: string;
10
+ INSTALL_PS1_URL: string;
11
+ fetchLatestVersion: typeof fetchLatestVersion;
12
+ detectInstallMethod: typeof detectInstallMethod;
13
+ };
14
+ export {};
@@ -1,15 +1,20 @@
1
1
  import { Command } from 'commander';
2
2
  import { execFileSync } from 'child_process';
3
+ import { platform as osPlatform, tmpdir } from 'os';
4
+ import { mkdtempSync, writeFileSync, rmSync } from 'fs';
5
+ import { join } from 'path';
3
6
  import { VERSION } from '../generated/version.js';
4
- const GITHUB_REPO = 'runwork-ai/cli';
5
- const INSTALL_SCRIPT_URL = 'https://runwork.ai/install.sh';
7
+ const BASE_URL = 'https://runwork.ai';
8
+ const LATEST_JSON_URL = `${BASE_URL}/cli/latest.json`;
9
+ const INSTALL_SH_URL = `${BASE_URL}/install.sh`;
10
+ const INSTALL_PS1_URL = `${BASE_URL}/install.ps1`;
6
11
  async function fetchLatestVersion() {
7
12
  try {
8
- const response = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`);
13
+ const response = await fetch(LATEST_JSON_URL, { cache: 'no-store' });
9
14
  if (!response.ok)
10
15
  return null;
11
16
  const data = await response.json();
12
- return data.tag_name?.replace(/^v/, '') || null;
17
+ return data.version?.replace(/^v/, '') || null;
13
18
  }
14
19
  catch {
15
20
  return null;
@@ -48,6 +53,44 @@ function detectInstallMethod() {
48
53
  }
49
54
  return 'binary';
50
55
  }
56
+ async function upgradeBinary() {
57
+ const isWindows = osPlatform() === 'win32';
58
+ if (isWindows) {
59
+ console.log('\nUpgrading via PowerShell installer...\n');
60
+ const response = await fetch(INSTALL_PS1_URL, { cache: 'no-store' });
61
+ if (!response.ok) {
62
+ throw new Error(`Failed to download install.ps1: HTTP ${response.status}`);
63
+ }
64
+ const script = await response.text();
65
+ // Write the script to a temp file and invoke it with `-File`. This is more
66
+ // robust than `-Command` because it avoids PowerShell's quoting rules for
67
+ // multi-line strings with embedded single or double quotes.
68
+ const tmpDir = mkdtempSync(join(tmpdir(), 'runwork-upgrade-'));
69
+ const scriptPath = join(tmpDir, 'install.ps1');
70
+ try {
71
+ writeFileSync(scriptPath, script);
72
+ execFileSync('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', scriptPath], { stdio: 'inherit' });
73
+ }
74
+ finally {
75
+ try {
76
+ rmSync(tmpDir, { recursive: true, force: true });
77
+ }
78
+ catch {
79
+ /* best-effort cleanup */
80
+ }
81
+ }
82
+ console.log('\nUpgraded successfully.');
83
+ return;
84
+ }
85
+ console.log('\nUpgrading via install.sh...\n');
86
+ const response = await fetch(INSTALL_SH_URL, { cache: 'no-store' });
87
+ if (!response.ok) {
88
+ throw new Error(`Failed to download install.sh: HTTP ${response.status}`);
89
+ }
90
+ const script = await response.text();
91
+ execFileSync('sh', ['-s'], { input: script, stdio: ['pipe', 'inherit', 'inherit'] });
92
+ console.log('\nUpgraded successfully.');
93
+ }
51
94
  export const upgradeCommand = new Command('upgrade')
52
95
  .description('Upgrade Runwork CLI to the latest version')
53
96
  .option('--check', 'Check for updates without installing')
@@ -55,7 +98,8 @@ export const upgradeCommand = new Command('upgrade')
55
98
  console.log(`Current version: ${VERSION}`);
56
99
  const latest = await fetchLatestVersion();
57
100
  if (!latest) {
58
- console.error('Could not fetch latest version. Check your internet connection.');
101
+ console.error(`Could not fetch latest version from ${LATEST_JSON_URL}.`);
102
+ console.error('Check your network and that runwork.ai is reachable.');
59
103
  process.exit(1);
60
104
  }
61
105
  if (latest === VERSION) {
@@ -147,19 +191,26 @@ export const upgradeCommand = new Command('upgrade')
147
191
  }
148
192
  return;
149
193
  }
150
- // Binary install: download install script and pipe to sh
151
- console.log('\nUpgrading via install script...\n');
194
+ // Binary install: first-party runwork.ai installer.
152
195
  try {
153
- const response = await fetch(INSTALL_SCRIPT_URL);
154
- if (!response.ok) {
155
- throw new Error(`Failed to download install script: ${response.status}`);
156
- }
157
- const script = await response.text();
158
- execFileSync('sh', ['-c', script], { stdio: 'inherit' });
159
- console.log('\nUpgraded successfully.');
196
+ await upgradeBinary();
160
197
  }
161
- catch (error) {
162
- console.error(`\nUpgrade failed. Try manually: curl -fsSL ${INSTALL_SCRIPT_URL} | sh`);
198
+ catch (err) {
199
+ const message = err instanceof Error ? err.message : String(err);
200
+ console.error(`\nUpgrade failed: ${message}`);
201
+ const manualCmd = osPlatform() === 'win32'
202
+ ? `irm ${INSTALL_PS1_URL} | iex`
203
+ : `curl -fsSL ${INSTALL_SH_URL} | sh`;
204
+ console.error(`Try manually: ${manualCmd}`);
163
205
  process.exit(1);
164
206
  }
165
207
  });
208
+ // Exported for tests
209
+ export const __internal = {
210
+ BASE_URL,
211
+ LATEST_JSON_URL,
212
+ INSTALL_SH_URL,
213
+ INSTALL_PS1_URL,
214
+ fetchLatestVersion,
215
+ detectInstallMethod,
216
+ };