runwork 0.6.1 → 0.8.2
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/__tests__/install-scripts.test.d.ts +1 -0
- package/dist/__tests__/install-scripts.test.js +247 -0
- package/dist/agents/__tests__/claude-code-plugin.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-code-plugin.test.js +47 -0
- package/dist/agents/__tests__/claude-code-stats.test.js +143 -78
- package/dist/agents/__tests__/claude-desktop-plugin-tree.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-desktop-plugin-tree.test.js +161 -0
- package/dist/agents/__tests__/claude-desktop-plugin.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-desktop-plugin.test.js +117 -0
- package/dist/agents/__tests__/claude-desktop-rpm.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-desktop-rpm.test.js +126 -0
- package/dist/agents/__tests__/codex-instructions.test.d.ts +1 -0
- package/dist/agents/__tests__/codex-instructions.test.js +57 -0
- package/dist/agents/__tests__/codex-stats.test.js +9 -0
- package/dist/agents/__tests__/graceful-degradation.test.js +3 -3
- package/dist/agents/__tests__/intro-skill.test.js +11 -2
- package/dist/agents/__tests__/skill-slug.test.d.ts +1 -0
- package/dist/agents/__tests__/skill-slug.test.js +77 -0
- package/dist/agents/claude-code.d.ts +3 -1
- package/dist/agents/claude-code.js +233 -58
- package/dist/agents/claude-desktop-plugin-tree.d.ts +44 -0
- package/dist/agents/claude-desktop-plugin-tree.js +92 -0
- package/dist/agents/claude-desktop.d.ts +20 -1
- package/dist/agents/claude-desktop.js +238 -61
- package/dist/agents/cline.d.ts +3 -1
- package/dist/agents/cline.js +44 -2
- package/dist/agents/codex.d.ts +3 -1
- package/dist/agents/codex.js +85 -14
- package/dist/agents/cursor.d.ts +12 -1
- package/dist/agents/cursor.js +98 -25
- package/dist/agents/gemini.d.ts +3 -1
- package/dist/agents/gemini.js +42 -4
- package/dist/agents/generic-adapter.d.ts +3 -1
- package/dist/agents/generic-adapter.js +59 -4
- package/dist/agents/intro-skill.js +77 -31
- package/dist/agents/types.d.ts +42 -2
- package/dist/agents/types.js +61 -6
- package/dist/agents/utils/instruction-hint.d.ts +10 -0
- package/dist/agents/utils/instruction-hint.js +46 -0
- package/dist/agents/utils/json-config.d.ts +6 -1
- package/dist/agents/utils/json-config.js +31 -4
- package/dist/agents/windsurf.d.ts +3 -1
- package/dist/agents/windsurf.js +37 -2
- package/dist/commands/__tests__/sync-telemetry.test.d.ts +1 -0
- package/dist/commands/__tests__/sync-telemetry.test.js +281 -0
- package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
- package/dist/commands/__tests__/upgrade.test.js +174 -0
- package/dist/commands/build-plugin.d.ts +2 -0
- package/dist/commands/build-plugin.js +135 -0
- package/dist/commands/init.d.ts +8 -2
- package/dist/commands/init.js +19 -7
- package/dist/commands/sync-telemetry.d.ts +33 -0
- package/dist/commands/sync-telemetry.js +186 -0
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +84 -118
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.js +145 -0
- package/dist/commands/upgrade.d.ts +12 -0
- package/dist/commands/upgrade.js +67 -16
- package/dist/generated/bundled-types.js +36 -36
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/health/__tests__/cli-distribution-checks.test.d.ts +1 -0
- package/dist/health/__tests__/cli-distribution-checks.test.js +199 -0
- package/dist/health/checks.d.ts +2 -0
- package/dist/health/checks.js +131 -18
- package/dist/health/runner.js +6 -2
- package/dist/index.js +4 -0
- package/dist/sync/executor.d.ts +2 -0
- package/dist/sync/executor.js +8 -4
- package/dist/types.d.ts +2 -0
- package/dist/ui/banner.js +15 -6
- package/dist/utils/__tests__/sqlite.test.js +84 -72
- package/dist/utils/sqlite-adapter.d.ts +48 -0
- package/dist/utils/sqlite-adapter.js +147 -0
- package/dist/utils/sqlite.d.ts +12 -5
- package/dist/utils/sqlite.js +12 -22
- package/dist/utils/zip.d.ts +6 -0
- package/dist/utils/zip.js +28 -3
- package/package.json +2 -4
package/dist/commands/sync.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
state.
|
|
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,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 {};
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -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
|
|
5
|
-
const
|
|
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(
|
|
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.
|
|
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(
|
|
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:
|
|
151
|
-
console.log('\nUpgrading via install script...\n');
|
|
194
|
+
// Binary install: first-party runwork.ai installer.
|
|
152
195
|
try {
|
|
153
|
-
|
|
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 (
|
|
162
|
-
|
|
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
|
+
};
|