runwork 0.2.0 → 0.2.5

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/LICENSE CHANGED
@@ -1,21 +1,15 @@
1
- MIT License
1
+ Copyright (c) 2025-2026 Runwork All rights reserved.
2
2
 
3
- Copyright (c) 2025 Runwork
3
+ This software and associated documentation files (the "Software") are the
4
+ proprietary and confidential property of Runwork
4
5
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
6
+ No part of the Software may be copied, modified, distributed, sublicensed,
7
+ sold, or otherwise made available to any third party without the prior
8
+ written consent of Runwork
11
9
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
10
+ You may use the Software solely as a tool to interact with the Runwork
11
+ platform in accordance with the Runwork Terms of Service.
14
12
 
15
13
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
14
+ IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE.
@@ -63,6 +63,28 @@ export declare class ApiClient {
63
63
  filePath: string;
64
64
  fileContents: string;
65
65
  }>, deletedFiles?: string[]): Promise<void>;
66
+ searchIntegrations(query: string, limit?: number): Promise<{
67
+ results: Array<{
68
+ id: string;
69
+ name: string;
70
+ provider: string;
71
+ docs?: {
72
+ name: string;
73
+ baseUrl: string;
74
+ pathFormat: string;
75
+ examples: string[];
76
+ notes?: string[];
77
+ requiredSetup?: string;
78
+ };
79
+ }>;
80
+ }>;
81
+ listConnectedIntegrations(workspaceId: string): Promise<Array<{
82
+ integrationId: string;
83
+ canonicalId?: string;
84
+ provider: string;
85
+ status?: string;
86
+ createdAt?: string;
87
+ }>>;
66
88
  /**
67
89
  * Get git remote URL for an app.
68
90
  * Both canonical ({baseUrl}/api/git/{workspaceId}/{appId}) and
@@ -133,6 +133,15 @@ export class ApiClient {
133
133
  body: JSON.stringify({ files, deletedFiles }),
134
134
  });
135
135
  }
136
+ async searchIntegrations(query, limit = 20) {
137
+ const params = new URLSearchParams({ q: query, limit: String(limit) });
138
+ const res = await this.request(`/api/integrations/search?${params}`);
139
+ return res.data;
140
+ }
141
+ async listConnectedIntegrations(workspaceId) {
142
+ const res = await this.request(`/api/workspaces/${workspaceId}/integrations`);
143
+ return res.data.integrations;
144
+ }
136
145
  /**
137
146
  * Get git remote URL for an app.
138
147
  * Both canonical ({baseUrl}/api/git/{workspaceId}/{appId}) and
@@ -1,10 +1,11 @@
1
1
  import { Command } from 'commander';
2
2
  import { execFileSync } from 'child_process';
3
- import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
3
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
4
4
  import { join } from 'path';
5
5
  import { requireAuth } from '../auth/store.js';
6
6
  import { ApiClient } from '../api/client.js';
7
7
  import { watchAndAutoCommit, stopAutoCommit } from '../git/auto-commit.js';
8
+ import { syncWithRemote } from '../git/sync.js';
8
9
  import { startLogTailer } from '../logs/tailer.js';
9
10
  import { populateTypes } from '../types-manager.js';
10
11
  import { loadManifest, generateManifest, saveManifest, detectUserEdits } from '../template/manifest.js';
@@ -28,25 +29,6 @@ function readConfig() {
28
29
  }
29
30
  return JSON.parse(readFileSync('.runwork.json', 'utf-8'));
30
31
  }
31
- function hasCommits() {
32
- try {
33
- execFileSync('git', ['rev-parse', 'HEAD'], { stdio: 'pipe' });
34
- return true;
35
- }
36
- catch {
37
- return false;
38
- }
39
- }
40
- function hasTrackedChanges() {
41
- try {
42
- // Only check tracked files (modified/deleted/staged) - not untracked (??) files
43
- const output = execFileSync('git', ['status', '--porcelain'], { encoding: 'utf-8' });
44
- return output.trim().split('\n').some(line => line.length > 0 && !line.startsWith('??'));
45
- }
46
- catch {
47
- return false;
48
- }
49
- }
50
32
  export const devCommand = new Command('dev')
51
33
  .description('Start local development server with live sync')
52
34
  .option('--no-logs', 'Disable automatic log tailing')
@@ -103,60 +85,28 @@ export const devCommand = new Command('dev')
103
85
  // Fetch SKILL.md (best-effort, after session so DO registries are available)
104
86
  await populateSkill(cwd, client, config.appId);
105
87
  // Sync AFTER starting session so we pick up any commits the DO created
106
- if (hasCommits()) {
107
- console.log('Syncing with Runwork...');
108
- const dirty = hasTrackedChanges();
109
- if (dirty) {
110
- execFileSync('git', ['stash', 'push', '-m', 'runwork-dev-sync'], { stdio: 'inherit' });
111
- }
112
- try {
113
- // Fetch first, then remove untracked skeleton files that conflict
114
- // with the remote before rebasing. Skeleton files are ephemeral
115
- // (re-downloaded each session) so the server's versions take precedence.
116
- // User-edited files are already tracked/committed at this point.
117
- execFileSync('git', ['fetch', 'runwork', 'main'], { stdio: 'inherit' });
118
- try {
119
- const remoteFiles = execFileSync('git', ['ls-tree', '-r', '--name-only', 'runwork/main'], { encoding: 'utf-8' }).trim().split('\n');
120
- const untrackedOutput = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], { encoding: 'utf-8' }).trim();
121
- const untracked = new Set(untrackedOutput.split('\n').filter(Boolean));
122
- for (const file of remoteFiles) {
123
- if (untracked.has(file)) {
124
- try {
125
- unlinkSync(join(cwd, file));
126
- }
127
- catch { /* already gone */ }
128
- }
129
- }
130
- }
131
- catch { /* best effort */ }
132
- execFileSync('git', ['rebase', 'runwork/main'], { stdio: 'inherit' });
133
- }
134
- catch {
135
- try {
136
- execFileSync('git', ['rebase', '--abort'], { stdio: 'pipe' });
137
- }
138
- catch { /* no rebase in progress */ }
139
- console.warn('Pull failed (remote may not have commits yet). Continuing...');
140
- }
141
- if (dirty) {
142
- try {
143
- execFileSync('git', ['stash', 'pop'], { stdio: 'inherit' });
144
- }
145
- catch {
88
+ console.log('Syncing with Runwork...');
89
+ const syncResult = syncWithRemote(cwd);
90
+ switch (syncResult.status) {
91
+ case 'skipped':
92
+ console.log('No commits yet. Skipping sync.');
93
+ break;
94
+ case 'synced':
95
+ console.log('Synced with Runwork.');
96
+ break;
97
+ case 'merged':
98
+ console.log('Merged with Runwork (histories diverged).');
99
+ break;
100
+ case 'sync-failed':
101
+ if (syncResult.error === 'stash-conflict') {
146
102
  console.error('Merge conflicts detected after stash pop. Resolve conflicts and restart.');
147
103
  process.exit(1);
148
104
  }
149
- }
150
- // Push local changes
151
- try {
152
- execFileSync('git', ['push', 'runwork', 'main'], { stdio: 'inherit' });
153
- }
154
- catch {
155
- console.warn('Push failed. Continuing with current state...');
156
- }
105
+ console.warn('Pull failed (remote may not have commits yet). Continuing...');
106
+ break;
157
107
  }
158
- else {
159
- console.log('No commits yet. Skipping sync.');
108
+ if (!syncResult.pushed && syncResult.status !== 'skipped' && syncResult.status !== 'sync-failed') {
109
+ console.warn('Push failed. Continuing with current state...');
160
110
  }
161
111
  // Declare logTailer before cleanup so cleanup can reference it
162
112
  let logTailer;
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const integrationsCommand: Command;
@@ -0,0 +1,86 @@
1
+ import { Command } from 'commander';
2
+ import { requireAuth } from '../auth/store.js';
3
+ import { ApiClient } from '../api/client.js';
4
+ import { readFileSync, existsSync } from 'fs';
5
+ function readConfig() {
6
+ if (!existsSync('.runwork.json')) {
7
+ console.error('No .runwork.json found. Run `runwork init` first.');
8
+ process.exit(1);
9
+ }
10
+ return JSON.parse(readFileSync('.runwork.json', 'utf-8'));
11
+ }
12
+ const searchCommand = new Command('search')
13
+ .description('Search available integrations from the platform catalog')
14
+ .argument('<query>', 'Search query (e.g., "google drive", "hubspot", "slack")')
15
+ .option('--limit <n>', 'Maximum results to show', '20')
16
+ .action(async (query, opts) => {
17
+ const credentials = requireAuth();
18
+ const client = new ApiClient(credentials);
19
+ const limit = parseInt(opts.limit, 10);
20
+ try {
21
+ const { results } = await client.searchIntegrations(query, limit);
22
+ if (results.length === 0) {
23
+ console.log(`No integrations found for "${query}".`);
24
+ console.log('Try broader terms (e.g., "calendar" instead of "google calendar").');
25
+ return;
26
+ }
27
+ console.log(`Found ${results.length} integration(s) for "${query}":\n`);
28
+ for (const r of results) {
29
+ console.log(` ${r.id} - ${r.name} (${r.provider})`);
30
+ if (r.docs) {
31
+ console.log(` Base URL: ${r.docs.baseUrl}`);
32
+ console.log(` Path format: ${r.docs.pathFormat}`);
33
+ if (r.docs.examples && r.docs.examples.length > 0) {
34
+ console.log(` Endpoints:`);
35
+ for (const example of r.docs.examples) {
36
+ console.log(` ${example}`);
37
+ }
38
+ }
39
+ if (r.docs.notes && r.docs.notes.length > 0) {
40
+ for (const note of r.docs.notes) {
41
+ console.log(` Note: ${note}`);
42
+ }
43
+ }
44
+ if (r.docs.requiredSetup) {
45
+ console.log(` Setup: ${r.docs.requiredSetup}`);
46
+ }
47
+ }
48
+ console.log('');
49
+ }
50
+ console.log('\nUsage: Add the integration ID to APP_INTEGRATION_REQUIREMENTS in worker/integration-requirements.ts');
51
+ }
52
+ catch (err) {
53
+ console.error('Failed to search integrations:', err instanceof Error ? err.message : err);
54
+ process.exit(1);
55
+ }
56
+ });
57
+ const listCommand = new Command('list')
58
+ .description('List connected workspace integrations')
59
+ .action(async () => {
60
+ const credentials = requireAuth();
61
+ const client = new ApiClient(credentials);
62
+ const config = readConfig();
63
+ try {
64
+ const integrations = await client.listConnectedIntegrations(config.workspaceId);
65
+ if (integrations.length === 0) {
66
+ console.log('No integrations connected in this workspace.');
67
+ console.log('Connect integrations at https://runwork.ai/workspace-settings');
68
+ return;
69
+ }
70
+ console.log(`Connected integrations (${integrations.length}):\n`);
71
+ for (const i of integrations) {
72
+ const id = i.canonicalId || i.integrationId;
73
+ const status = i.status || 'connected';
74
+ const date = i.createdAt ? ` (since ${new Date(i.createdAt).toLocaleDateString()})` : '';
75
+ console.log(` ${id} - ${i.provider}${date} [${status}]`);
76
+ }
77
+ }
78
+ catch (err) {
79
+ console.error('Failed to list integrations:', err instanceof Error ? err.message : err);
80
+ process.exit(1);
81
+ }
82
+ });
83
+ export const integrationsCommand = new Command('integrations')
84
+ .description('Search and manage workspace integrations')
85
+ .addCommand(searchCommand)
86
+ .addCommand(listCommand);
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const upgradeCommand: Command;
@@ -0,0 +1,165 @@
1
+ import { Command } from 'commander';
2
+ import { execFileSync } from 'child_process';
3
+ import { VERSION } from '../generated/version.js';
4
+ const GITHUB_REPO = 'runwork-ai/cli';
5
+ const INSTALL_SCRIPT_URL = 'https://runwork.ai/install.sh';
6
+ async function fetchLatestVersion() {
7
+ try {
8
+ const response = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`);
9
+ if (!response.ok)
10
+ return null;
11
+ const data = await response.json();
12
+ return data.tag_name?.replace(/^v/, '') || null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ function detectInstallMethod() {
19
+ try {
20
+ const binaryPath = process.argv[1] || '';
21
+ // Homebrew: /opt/homebrew/Cellar/runwork/... or /usr/local/Cellar/runwork/...
22
+ if (binaryPath.includes('/Cellar/runwork/') || binaryPath.includes('/homebrew/')) {
23
+ return 'brew';
24
+ }
25
+ // Volta: ~/.volta/tools/image/packages/runwork/...
26
+ if (binaryPath.includes('/.volta/')) {
27
+ return 'volta';
28
+ }
29
+ // Bun global: ~/.bun/install/global/... or ~/.bun/bin/...
30
+ if (binaryPath.includes('/.bun/')) {
31
+ return 'bun';
32
+ }
33
+ // pnpm global: ~/.local/share/pnpm/... or pnpm/global/...
34
+ if (binaryPath.includes('/pnpm/') || binaryPath.includes('/.pnpm/')) {
35
+ return 'pnpm';
36
+ }
37
+ // Yarn global: ~/.yarn/bin/... or ~/.config/yarn/global/...
38
+ if (binaryPath.includes('/.yarn/') || binaryPath.includes('/yarn/global/')) {
39
+ return 'yarn';
40
+ }
41
+ // npm global: node_modules or /lib/node/ in path
42
+ if (binaryPath.includes('node_modules') || binaryPath.includes('/lib/node/')) {
43
+ return 'npm';
44
+ }
45
+ }
46
+ catch {
47
+ // Fall through to binary
48
+ }
49
+ return 'binary';
50
+ }
51
+ export const upgradeCommand = new Command('upgrade')
52
+ .description('Upgrade Runwork CLI to the latest version')
53
+ .option('--check', 'Check for updates without installing')
54
+ .action(async (options) => {
55
+ console.log(`Current version: ${VERSION}`);
56
+ const latest = await fetchLatestVersion();
57
+ if (!latest) {
58
+ console.error('Could not fetch latest version. Check your internet connection.');
59
+ process.exit(1);
60
+ }
61
+ if (latest === VERSION) {
62
+ console.log(`Already on the latest version (${VERSION}).`);
63
+ return;
64
+ }
65
+ console.log(`Latest version: ${latest}`);
66
+ if (options.check) {
67
+ console.log(`\nRun \`runwork upgrade\` to install the update.`);
68
+ return;
69
+ }
70
+ const method = detectInstallMethod();
71
+ if (method === 'brew') {
72
+ console.log('\nDetected installation method: brew');
73
+ console.log('Upgrading via Homebrew...\n');
74
+ try {
75
+ execFileSync('brew', ['update', '--quiet'], { stdio: 'inherit' });
76
+ execFileSync('brew', ['upgrade', 'runwork'], { stdio: 'inherit' });
77
+ console.log('\nUpgraded successfully via Homebrew.');
78
+ }
79
+ catch {
80
+ console.error('\nHomebrew upgrade failed. Try manually: brew update && brew upgrade runwork');
81
+ process.exit(1);
82
+ }
83
+ return;
84
+ }
85
+ if (method === 'bun') {
86
+ console.log('\nDetected installation method: bun');
87
+ console.log('Upgrading via bun...\n');
88
+ try {
89
+ execFileSync('bun', ['install', '-g', 'runwork@latest'], { stdio: 'inherit' });
90
+ console.log('\nUpgraded successfully via bun.');
91
+ }
92
+ catch {
93
+ console.error('\nbun upgrade failed. Try manually: bun install -g runwork@latest');
94
+ process.exit(1);
95
+ }
96
+ return;
97
+ }
98
+ if (method === 'pnpm') {
99
+ console.log('\nDetected installation method: pnpm');
100
+ console.log('Upgrading via pnpm...\n');
101
+ try {
102
+ execFileSync('pnpm', ['update', '-g', 'runwork'], { stdio: 'inherit' });
103
+ console.log('\nUpgraded successfully via pnpm.');
104
+ }
105
+ catch {
106
+ console.error('\npnpm upgrade failed. Try manually: pnpm update -g runwork');
107
+ process.exit(1);
108
+ }
109
+ return;
110
+ }
111
+ if (method === 'yarn') {
112
+ console.log('\nDetected installation method: yarn');
113
+ console.log('Upgrading via yarn...\n');
114
+ try {
115
+ execFileSync('yarn', ['global', 'upgrade', 'runwork'], { stdio: 'inherit' });
116
+ console.log('\nUpgraded successfully via yarn.');
117
+ }
118
+ catch {
119
+ console.error('\nyarn upgrade failed. Try manually: yarn global upgrade runwork');
120
+ process.exit(1);
121
+ }
122
+ return;
123
+ }
124
+ if (method === 'volta') {
125
+ console.log('\nDetected installation method: volta');
126
+ console.log('Upgrading via Volta...\n');
127
+ try {
128
+ execFileSync('volta', ['install', 'runwork@latest'], { stdio: 'inherit' });
129
+ console.log('\nUpgraded successfully via Volta.');
130
+ }
131
+ catch {
132
+ console.error('\nVolta upgrade failed. Try manually: volta install runwork@latest');
133
+ process.exit(1);
134
+ }
135
+ return;
136
+ }
137
+ if (method === 'npm') {
138
+ console.log('\nDetected installation method: npm');
139
+ console.log('Upgrading via npm...\n');
140
+ try {
141
+ execFileSync('npm', ['update', '-g', 'runwork'], { stdio: 'inherit' });
142
+ console.log('\nUpgraded successfully via npm.');
143
+ }
144
+ catch {
145
+ console.error('\nnpm upgrade failed. Try manually: npm update -g runwork');
146
+ process.exit(1);
147
+ }
148
+ return;
149
+ }
150
+ // Binary install: download install script and pipe to sh
151
+ console.log('\nUpgrading via install script...\n');
152
+ 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.');
160
+ }
161
+ catch (error) {
162
+ console.error(`\nUpgrade failed. Try manually: curl -fsSL ${INSTALL_SCRIPT_URL} | sh`);
163
+ process.exit(1);
164
+ }
165
+ });