runwork 0.2.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/bin/runwork.js +2 -0
  4. package/bundled-types/agents.d.ts +3 -0
  5. package/bundled-types/ai.d.ts +2 -0
  6. package/bundled-types/app-registry.d.ts +74 -0
  7. package/bundled-types/channels.d.ts +2 -0
  8. package/bundled-types/components.d.ts +1 -0
  9. package/bundled-types/core-agent.d.ts +265 -0
  10. package/bundled-types/core-ai.d.ts +170 -0
  11. package/bundled-types/core-base-agent.d.ts +89 -0
  12. package/bundled-types/core-channels.d.ts +42 -0
  13. package/bundled-types/core-components.d.ts +21 -0
  14. package/bundled-types/core-endpoints.d.ts +198 -0
  15. package/bundled-types/core-entities.d.ts +276 -0
  16. package/bundled-types/core-entity-do.d.ts +119 -0
  17. package/bundled-types/core-events.d.ts +52 -0
  18. package/bundled-types/core-file-storage.d.ts +216 -0
  19. package/bundled-types/core-integration-entities.d.ts +147 -0
  20. package/bundled-types/core-integrations.d.ts +143 -0
  21. package/bundled-types/core-routes.d.ts +33 -0
  22. package/bundled-types/core-scheduler.d.ts +180 -0
  23. package/bundled-types/core-utils.d.ts +85 -0
  24. package/bundled-types/core-workflow-cloudflare.d.ts +85 -0
  25. package/bundled-types/core-workflow-config.d.ts +18 -0
  26. package/bundled-types/core-workflow-coordinator.d.ts +78 -0
  27. package/bundled-types/core-workflow-instance.d.ts +82 -0
  28. package/bundled-types/core-workflow-types.d.ts +251 -0
  29. package/bundled-types/core-workflows.d.ts +75 -0
  30. package/bundled-types/core-workspace.d.ts +454 -0
  31. package/bundled-types/create-app.d.ts +57 -0
  32. package/bundled-types/endpoints.d.ts +2 -0
  33. package/bundled-types/entities.d.ts +5 -0
  34. package/bundled-types/events.d.ts +2 -0
  35. package/bundled-types/index.d.ts +19 -0
  36. package/bundled-types/integrations.d.ts +2 -0
  37. package/bundled-types/routes.d.ts +2 -0
  38. package/bundled-types/scheduler.d.ts +2 -0
  39. package/bundled-types/storage.d.ts +2 -0
  40. package/bundled-types/test-entry.d.ts +21 -0
  41. package/bundled-types/types.d.ts +1 -0
  42. package/bundled-types/vite.d.ts +23 -0
  43. package/bundled-types/workflows.d.ts +14 -0
  44. package/bundled-types/workspace.d.ts +2 -0
  45. package/dist/__tests__/types-manager.test.d.ts +1 -0
  46. package/dist/__tests__/types-manager.test.js +85 -0
  47. package/dist/api/__tests__/client.test.d.ts +1 -0
  48. package/dist/api/__tests__/client.test.js +264 -0
  49. package/dist/api/client.d.ts +72 -0
  50. package/dist/api/client.js +144 -0
  51. package/dist/auth/__tests__/store.test.d.ts +1 -0
  52. package/dist/auth/__tests__/store.test.js +103 -0
  53. package/dist/auth/browser.d.ts +2 -0
  54. package/dist/auth/browser.js +7 -0
  55. package/dist/auth/store.d.ts +5 -0
  56. package/dist/auth/store.js +33 -0
  57. package/dist/commands/clone.d.ts +2 -0
  58. package/dist/commands/clone.js +90 -0
  59. package/dist/commands/deploy.d.ts +2 -0
  60. package/dist/commands/deploy.js +43 -0
  61. package/dist/commands/dev.d.ts +2 -0
  62. package/dist/commands/dev.js +188 -0
  63. package/dist/commands/init.d.ts +2 -0
  64. package/dist/commands/init.js +87 -0
  65. package/dist/commands/login.d.ts +2 -0
  66. package/dist/commands/login.js +32 -0
  67. package/dist/commands/logout.d.ts +2 -0
  68. package/dist/commands/logout.js +13 -0
  69. package/dist/commands/logs.d.ts +2 -0
  70. package/dist/commands/logs.js +166 -0
  71. package/dist/generated/bundled-types.d.ts +1 -0
  72. package/dist/generated/bundled-types.js +44 -0
  73. package/dist/generated/version.d.ts +1 -0
  74. package/dist/generated/version.js +2 -0
  75. package/dist/git/__tests__/credentials.test.d.ts +1 -0
  76. package/dist/git/__tests__/credentials.test.js +128 -0
  77. package/dist/git/auto-commit.d.ts +3 -0
  78. package/dist/git/auto-commit.js +209 -0
  79. package/dist/git/credentials.d.ts +15 -0
  80. package/dist/git/credentials.js +65 -0
  81. package/dist/index.d.ts +1 -0
  82. package/dist/index.js +31 -0
  83. package/dist/logs/tailer.d.ts +13 -0
  84. package/dist/logs/tailer.js +135 -0
  85. package/dist/template/manifest.d.ts +17 -0
  86. package/dist/template/manifest.js +102 -0
  87. package/dist/types-manager.d.ts +1 -0
  88. package/dist/types-manager.js +35 -0
  89. package/dist/types.d.ts +27 -0
  90. package/dist/types.js +1 -0
  91. package/dist/utils/__tests__/prompt.test.d.ts +1 -0
  92. package/dist/utils/__tests__/prompt.test.js +103 -0
  93. package/dist/utils/fs.d.ts +5 -0
  94. package/dist/utils/fs.js +23 -0
  95. package/dist/utils/prompt.d.ts +4 -0
  96. package/dist/utils/prompt.js +34 -0
  97. package/dist/utils/zip.d.ts +1 -0
  98. package/dist/utils/zip.js +17 -0
  99. package/package.json +66 -0
@@ -0,0 +1,7 @@
1
+ export async function openBrowserForAuth(_authUrl) {
2
+ // TODO: implement browser-based auth flow
3
+ }
4
+ export async function startAuthCallbackServer(_port) {
5
+ // TODO: implement local callback server for OAuth
6
+ return '';
7
+ }
@@ -0,0 +1,5 @@
1
+ import type { Credentials } from '../types.js';
2
+ export declare function getCredentials(): Credentials | null;
3
+ export declare function saveCredentials(credentials: Credentials): void;
4
+ export declare function clearCredentials(): void;
5
+ export declare function requireAuth(): Credentials;
@@ -0,0 +1,33 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ const RUNWORK_DIR = join(homedir(), '.runwork');
5
+ const CREDENTIALS_FILE = join(RUNWORK_DIR, '.credentials');
6
+ export function getCredentials() {
7
+ if (!existsSync(CREDENTIALS_FILE))
8
+ return null;
9
+ try {
10
+ const raw = readFileSync(CREDENTIALS_FILE, 'utf-8');
11
+ return JSON.parse(raw);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export function saveCredentials(credentials) {
18
+ mkdirSync(RUNWORK_DIR, { recursive: true });
19
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 0o600 });
20
+ }
21
+ export function clearCredentials() {
22
+ if (existsSync(CREDENTIALS_FILE)) {
23
+ unlinkSync(CREDENTIALS_FILE);
24
+ }
25
+ }
26
+ export function requireAuth() {
27
+ const creds = getCredentials();
28
+ if (!creds) {
29
+ console.error('Not logged in. Run `runwork login` first.');
30
+ process.exit(1);
31
+ }
32
+ return creds;
33
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const cloneCommand: Command;
@@ -0,0 +1,90 @@
1
+ import { Command } from 'commander';
2
+ import { execFileSync } from 'child_process';
3
+ import { writeFileSync, mkdirSync, existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { requireAuth } from '../auth/store.js';
6
+ import { ApiClient } from '../api/client.js';
7
+ import { promptSelect } from '../utils/prompt.js';
8
+ import { generateManifest, saveManifest } from '../template/manifest.js';
9
+ import { extractZip } from '../utils/zip.js';
10
+ import { removeNestedGitDirs } from '../utils/fs.js';
11
+ export const cloneCommand = new Command('clone')
12
+ .description('Clone a Runwork app to local development')
13
+ .argument('[appId]', 'App ID to clone (interactive if omitted)')
14
+ .argument('[directory]', 'Target directory')
15
+ .action(async (appId, directory) => {
16
+ const creds = requireAuth();
17
+ const client = new ApiClient(creds);
18
+ let app;
19
+ if (appId) {
20
+ app = await client.getApp(appId);
21
+ }
22
+ else {
23
+ const apps = await client.listApps();
24
+ if (apps.length === 0) {
25
+ console.error('No apps found.');
26
+ process.exit(1);
27
+ }
28
+ const choice = await promptSelect('Select app to clone:', apps.map(a => ({ label: `${a.name} (${a.workspaceName})`, value: a })));
29
+ app = choice.value;
30
+ }
31
+ const slug = app.slug || app.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
32
+ const dir = directory || slug;
33
+ const remoteUrl = client.getGitRemoteUrl(app.workspaceId, app.id);
34
+ console.log(`Cloning "${app.name}"...`);
35
+ // Step 1: Extract skeleton template as the base layer
36
+ console.log('Downloading project template...');
37
+ mkdirSync(dir, { recursive: true });
38
+ try {
39
+ const zipData = await client.downloadSkeleton();
40
+ extractZip(zipData, dir);
41
+ removeNestedGitDirs(dir);
42
+ }
43
+ catch {
44
+ console.warn('Failed to download template. Cloning git-tracked files only.');
45
+ }
46
+ // Generate template manifest BEFORE git overlay (captures pristine template checksums)
47
+ const manifest = await generateManifest(dir);
48
+ await saveManifest(dir, manifest);
49
+ // Step 2: Git clone into the directory (overlays changed files on top of template)
50
+ // Init git, add remote, fetch, and checkout to overlay on existing template files
51
+ if (!existsSync(join(dir, '.git'))) {
52
+ execFileSync('git', ['init'], { cwd: dir, stdio: 'pipe' });
53
+ }
54
+ try {
55
+ execFileSync('git', ['remote', 'set-url', 'runwork', remoteUrl], { cwd: dir, stdio: 'pipe' });
56
+ }
57
+ catch {
58
+ execFileSync('git', ['remote', 'add', 'runwork', remoteUrl], { cwd: dir, stdio: 'pipe' });
59
+ }
60
+ try {
61
+ execFileSync('git', ['fetch', 'runwork', 'main'], { cwd: dir, stdio: 'inherit' });
62
+ // Reset to remote HEAD - overlays git-tracked files on top of template
63
+ execFileSync('git', ['reset', '--mixed', 'runwork/main'], { cwd: dir, stdio: 'pipe' });
64
+ // Checkout git-tracked files (overwrites template files where they differ)
65
+ execFileSync('git', ['checkout', '.'], { cwd: dir, stdio: 'pipe' });
66
+ }
67
+ catch {
68
+ console.warn('No remote commits yet. Starting with template only.');
69
+ }
70
+ // Write .runwork.json config
71
+ const config = {
72
+ workspaceId: app.workspaceId,
73
+ appId: app.id,
74
+ appName: app.name,
75
+ };
76
+ writeFileSync(join(dir, '.runwork.json'), JSON.stringify(config, null, 2));
77
+ // Fetch SKILL.md (best-effort)
78
+ try {
79
+ const { skill } = await client.getAppSkill(app.id);
80
+ if (skill) {
81
+ writeFileSync(join(dir, 'SKILL.md'), skill, 'utf-8');
82
+ }
83
+ }
84
+ catch {
85
+ // App may be new with no registries yet
86
+ }
87
+ console.log(`\nApp "${app.name}" cloned to ${dir}/`);
88
+ console.log(`Remote: ${remoteUrl}`);
89
+ console.log(`\nNext: cd ${dir} && runwork dev`);
90
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const deployCommand: Command;
@@ -0,0 +1,43 @@
1
+ import { Command } from 'commander';
2
+ import { execFileSync } from 'child_process';
3
+ import { readFileSync, existsSync } from 'fs';
4
+ import { requireAuth } from '../auth/store.js';
5
+ import { ApiClient } from '../api/client.js';
6
+ export const deployCommand = new Command('deploy')
7
+ .description('Deploy the current app to Runwork')
8
+ .action(async () => {
9
+ if (!existsSync('.runwork.json')) {
10
+ console.error('No .runwork.json found. Run `runwork init` first.');
11
+ process.exit(1);
12
+ }
13
+ const config = JSON.parse(readFileSync('.runwork.json', 'utf-8'));
14
+ const creds = requireAuth();
15
+ const client = new ApiClient(creds);
16
+ // Sync: pull + push (skip if no commits yet)
17
+ let repoHasCommits = false;
18
+ try {
19
+ execFileSync('git', ['rev-parse', 'HEAD'], { stdio: 'pipe' });
20
+ repoHasCommits = true;
21
+ }
22
+ catch {
23
+ // No commits yet
24
+ }
25
+ if (repoHasCommits) {
26
+ console.log('Syncing...');
27
+ try {
28
+ execFileSync('git', ['pull', '--rebase', 'runwork', 'main'], { stdio: 'inherit' });
29
+ }
30
+ catch {
31
+ console.warn('Pull failed. Continuing...');
32
+ }
33
+ execFileSync('git', ['push', 'runwork', 'main'], { stdio: 'inherit' });
34
+ }
35
+ else {
36
+ console.error('No commits to deploy. Commit your changes first.');
37
+ process.exit(1);
38
+ }
39
+ // Trigger deploy
40
+ console.log('Deploying...');
41
+ const result = await client.triggerDeploy(config.appId);
42
+ console.log(`Deployed: ${result.deploymentUrl}`);
43
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const devCommand: Command;
@@ -0,0 +1,188 @@
1
+ import { Command } from 'commander';
2
+ import { execFileSync } from 'child_process';
3
+ import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { requireAuth } from '../auth/store.js';
6
+ import { ApiClient } from '../api/client.js';
7
+ import { watchAndAutoCommit, stopAutoCommit } from '../git/auto-commit.js';
8
+ import { startLogTailer } from '../logs/tailer.js';
9
+ import { populateTypes } from '../types-manager.js';
10
+ import { loadManifest, generateManifest, saveManifest, detectUserEdits } from '../template/manifest.js';
11
+ import { extractZip } from '../utils/zip.js';
12
+ import { removeNestedGitDirs } from '../utils/fs.js';
13
+ async function populateSkill(projectDir, client, appId) {
14
+ try {
15
+ const { skill } = await client.getAppSkill(appId);
16
+ if (skill) {
17
+ writeFileSync(join(projectDir, 'SKILL.md'), skill, 'utf-8');
18
+ }
19
+ }
20
+ catch {
21
+ // Best-effort: app may be new with no registries yet
22
+ }
23
+ }
24
+ function readConfig() {
25
+ if (!existsSync('.runwork.json')) {
26
+ console.error('No .runwork.json found. Run `runwork init` first.');
27
+ process.exit(1);
28
+ }
29
+ return JSON.parse(readFileSync('.runwork.json', 'utf-8'));
30
+ }
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
+ export const devCommand = new Command('dev')
51
+ .description('Start local development server with live sync')
52
+ .option('--no-logs', 'Disable automatic log tailing')
53
+ .option('--logs-only-file', 'Write logs to file only, not terminal')
54
+ .action(async (options) => {
55
+ const config = readConfig();
56
+ const creds = requireAuth();
57
+ const client = new ApiClient(creds);
58
+ const cwd = process.cwd();
59
+ // Detect user edits made outside of `runwork dev`
60
+ const oldManifest = await loadManifest(cwd);
61
+ if (oldManifest) {
62
+ const userEdits = await detectUserEdits(cwd, oldManifest);
63
+ if (userEdits.length > 0) {
64
+ console.log(`Detected ${userEdits.length} file(s) edited outside dev session:`);
65
+ for (const file of userEdits) {
66
+ console.log(` ${file}`);
67
+ }
68
+ try {
69
+ execFileSync('git', ['add', '--', ...userEdits], { stdio: 'pipe' });
70
+ execFileSync('git', ['commit', '-m', 'sync: user edits'], { stdio: 'pipe' });
71
+ }
72
+ catch {
73
+ // Nothing to commit or git not initialized yet
74
+ }
75
+ }
76
+ }
77
+ // Download fresh template to ensure latest version
78
+ console.log('Updating template...');
79
+ try {
80
+ const zipData = await client.downloadSkeleton();
81
+ extractZip(zipData, cwd);
82
+ removeNestedGitDirs(cwd);
83
+ // Generate new manifest from fresh template (before restoring user files)
84
+ const newManifest = await generateManifest(cwd);
85
+ await saveManifest(cwd, newManifest);
86
+ // Restore user-edited files from git on top of fresh template
87
+ try {
88
+ execFileSync('git', ['checkout', '--', '.'], { cwd, stdio: 'pipe' });
89
+ }
90
+ catch {
91
+ // May fail if no commits yet
92
+ }
93
+ }
94
+ catch {
95
+ console.warn('Template update failed. Continuing with current files.');
96
+ }
97
+ // Populate type definitions
98
+ await populateTypes(cwd);
99
+ // Start dev session first - this triggers DO onStart which may create remote commits
100
+ console.log('Starting dev session...');
101
+ const session = await client.startDevSession(config.appId);
102
+ console.log(`Preview: ${session.previewUrl}`);
103
+ // Fetch SKILL.md (best-effort, after session so DO registries are available)
104
+ await populateSkill(cwd, client, config.appId);
105
+ // 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 {
146
+ console.error('Merge conflicts detected after stash pop. Resolve conflicts and restart.');
147
+ process.exit(1);
148
+ }
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
+ }
157
+ }
158
+ else {
159
+ console.log('No commits yet. Skipping sync.');
160
+ }
161
+ // Declare logTailer before cleanup so cleanup can reference it
162
+ let logTailer;
163
+ // Register cleanup before starting watcher to avoid race
164
+ const cleanup = async () => {
165
+ console.log('\nStopping...');
166
+ logTailer?.stop();
167
+ await stopAutoCommit();
168
+ process.exit(0);
169
+ };
170
+ process.on('SIGINT', cleanup);
171
+ process.on('SIGTERM', cleanup);
172
+ // Start file watcher
173
+ console.log('Watching for file changes...');
174
+ await watchAndAutoCommit(process.cwd(), client, config.appId);
175
+ // Start log tailing (unless --no-logs)
176
+ if (options.logs !== false) {
177
+ logTailer = startLogTailer({
178
+ appId: config.appId,
179
+ client,
180
+ projectDir: cwd,
181
+ toTerminal: !options.logsOnlyFile,
182
+ toFile: true,
183
+ });
184
+ console.log('Log tailing active. Logs written to .runwork/logs.txt');
185
+ }
186
+ // Keep alive
187
+ await new Promise(() => { });
188
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const initCommand: Command;
@@ -0,0 +1,87 @@
1
+ import { Command } from 'commander';
2
+ import { execFileSync } from 'child_process';
3
+ import { writeFileSync, existsSync, mkdirSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { requireAuth } from '../auth/store.js';
6
+ import { ApiClient } from '../api/client.js';
7
+ import { promptSelect, promptInput } from '../utils/prompt.js';
8
+ import { generateManifest, saveManifest } from '../template/manifest.js';
9
+ import { extractZip } from '../utils/zip.js';
10
+ import { removeNestedGitDirs } from '../utils/fs.js';
11
+ export const initCommand = new Command('init')
12
+ .description('Initialize a new Runwork app')
13
+ .argument('[name]', 'App name')
14
+ .action(async (name) => {
15
+ const creds = requireAuth();
16
+ const client = new ApiClient(creds);
17
+ const appName = name || await promptInput('App name');
18
+ if (!appName) {
19
+ console.error('App name is required.');
20
+ process.exit(1);
21
+ }
22
+ const workspaces = await client.listWorkspaces();
23
+ if (workspaces.length === 0) {
24
+ console.error('No workspaces found. Create one at runwork.ai first.');
25
+ process.exit(1);
26
+ }
27
+ const workspaceChoice = await promptSelect('Select workspace:', workspaces.map(w => ({ label: w.name, value: w })));
28
+ const workspace = workspaceChoice.value;
29
+ console.log(`Initializing "${appName}" in workspace "${workspace.name}"...`);
30
+ const app = await client.initApp(workspace.id, appName);
31
+ const slug = app.slug || appName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
32
+ const dir = slug;
33
+ if (existsSync(dir)) {
34
+ console.error(`Directory "${dir}" already exists.`);
35
+ process.exit(1);
36
+ }
37
+ mkdirSync(dir, { recursive: true });
38
+ // Download and extract skeleton template first
39
+ console.log('Downloading project template...');
40
+ try {
41
+ const zipData = await client.downloadSkeleton();
42
+ extractZip(zipData, dir);
43
+ removeNestedGitDirs(dir);
44
+ console.log('Project template extracted.');
45
+ // Generate template manifest for tracking user edits
46
+ const manifest = await generateManifest(dir);
47
+ await saveManifest(dir, manifest);
48
+ }
49
+ catch {
50
+ console.warn('Failed to download skeleton template. Starting with empty project.');
51
+ }
52
+ // Write .runwork.json AFTER template extraction so it doesn't get overwritten
53
+ const config = {
54
+ workspaceId: workspace.id,
55
+ appId: app.id,
56
+ appName: app.name,
57
+ };
58
+ writeFileSync(join(dir, '.runwork.json'), JSON.stringify(config, null, 2));
59
+ // Initialize git
60
+ if (!existsSync(join(dir, '.git'))) {
61
+ execFileSync('git', ['init'], { cwd: dir, stdio: 'inherit' });
62
+ }
63
+ // Add runwork remote
64
+ const remoteUrl = client.getGitRemoteUrl(workspace.id, app.id);
65
+ try {
66
+ execFileSync('git', ['remote', 'add', 'runwork', remoteUrl], { cwd: dir, stdio: 'pipe' });
67
+ }
68
+ catch {
69
+ execFileSync('git', ['remote', 'set-url', 'runwork', remoteUrl], { cwd: dir, stdio: 'pipe' });
70
+ }
71
+ // Create .gitignore if needed
72
+ if (!existsSync(join(dir, '.gitignore'))) {
73
+ writeFileSync(join(dir, '.gitignore'), 'node_modules/\n.runwork/\n.dev.vars\n');
74
+ }
75
+ // Initial commit: only track .runwork.json and .gitignore (not template files)
76
+ execFileSync('git', ['add', '--', '.runwork.json', '.gitignore'], { cwd: dir, stdio: 'pipe' });
77
+ try {
78
+ execFileSync('git', ['commit', '-m', 'Initialize app'], { cwd: dir, stdio: 'pipe' });
79
+ }
80
+ catch {
81
+ // Nothing to commit is fine
82
+ }
83
+ execFileSync('git', ['push', '-u', 'runwork', 'main'], { cwd: dir, stdio: 'inherit' });
84
+ console.log(`\nApp "${app.name}" initialized in ${dir}/`);
85
+ console.log(`Remote: ${remoteUrl}`);
86
+ console.log(`\nNext: cd ${dir} && runwork dev`);
87
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const loginCommand: Command;
@@ -0,0 +1,32 @@
1
+ import { Command } from 'commander';
2
+ import { ApiClient } from '../api/client.js';
3
+ import { saveCredentials } from '../auth/store.js';
4
+ import { configureGitCredentials } from '../git/credentials.js';
5
+ export const loginCommand = new Command('login')
6
+ .description('Authenticate with Runwork platform')
7
+ .option('--base-url <url>', 'Platform URL', 'https://runwork.ai')
8
+ .action(async (options) => {
9
+ const client = new ApiClient({ apiKey: '', email: '', baseUrl: options.baseUrl });
10
+ console.log('Opening browser for authentication...');
11
+ const { sessionId, loginUrl } = await client.initiateLogin();
12
+ // Open browser
13
+ const open = await import('open');
14
+ await open.default(loginUrl);
15
+ console.log(`If browser didn't open, visit: ${loginUrl}`);
16
+ console.log('Waiting for authentication...');
17
+ // Poll for up to 5 minutes
18
+ const maxAttempts = 60;
19
+ const pollInterval = 5000;
20
+ for (let i = 0; i < maxAttempts; i++) {
21
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
22
+ const result = await client.pollLogin(sessionId);
23
+ if (result) {
24
+ saveCredentials(result);
25
+ await configureGitCredentials(result.baseUrl || options.baseUrl);
26
+ console.log(`Logged in as ${result.email}`);
27
+ return;
28
+ }
29
+ }
30
+ console.error('Login timed out. Please try again.');
31
+ process.exit(1);
32
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const logoutCommand: Command;
@@ -0,0 +1,13 @@
1
+ import { Command } from 'commander';
2
+ import { clearCredentials, getCredentials } from '../auth/store.js';
3
+ import { removeGitCredentials } from '../git/credentials.js';
4
+ export const logoutCommand = new Command('logout')
5
+ .description('Remove stored Runwork credentials')
6
+ .action(async () => {
7
+ const creds = getCredentials();
8
+ if (creds) {
9
+ await removeGitCredentials(creds.baseUrl);
10
+ }
11
+ clearCredentials();
12
+ console.log('Logged out.');
13
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const logsCommand: Command;