runwork 0.2.5 → 0.4.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.
- package/dist/api/client.d.ts +2 -1
- package/dist/api/client.js +4 -0
- package/dist/auth/login-flow.d.ts +10 -0
- package/dist/auth/login-flow.js +37 -0
- package/dist/commands/clone.d.ts +3 -0
- package/dist/commands/clone.js +32 -24
- package/dist/commands/deploy.js +24 -7
- package/dist/commands/dev.d.ts +5 -0
- package/dist/commands/dev.js +151 -45
- package/dist/commands/info.d.ts +2 -0
- package/dist/commands/info.js +432 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +29 -21
- package/dist/commands/integrations.js +19 -2
- package/dist/commands/login.js +10 -25
- package/dist/commands/logs.js +82 -23
- package/dist/commands/open.d.ts +2 -0
- package/dist/commands/open.js +53 -0
- package/dist/commands/welcome.d.ts +1 -0
- package/dist/commands/welcome.js +83 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/git/__tests__/sync.test.js +10 -8
- package/dist/git/auto-commit.d.ts +5 -1
- package/dist/git/auto-commit.js +15 -9
- package/dist/git/sync.js +46 -5
- package/dist/index.js +22 -1
- package/dist/logs/__tests__/tailer-format.test.d.ts +1 -0
- package/dist/logs/__tests__/tailer-format.test.js +43 -0
- package/dist/logs/tailer.d.ts +4 -0
- package/dist/logs/tailer.js +58 -10
- package/dist/types.d.ts +59 -0
- package/dist/ui/__tests__/banner.test.d.ts +1 -0
- package/dist/ui/__tests__/banner.test.js +82 -0
- package/dist/ui/__tests__/colors.test.d.ts +1 -0
- package/dist/ui/__tests__/colors.test.js +22 -0
- package/dist/ui/__tests__/keyboard.test.d.ts +1 -0
- package/dist/ui/__tests__/keyboard.test.js +30 -0
- package/dist/ui/__tests__/status-line.test.d.ts +1 -0
- package/dist/ui/__tests__/status-line.test.js +54 -0
- package/dist/ui/banner.d.ts +29 -0
- package/dist/ui/banner.js +118 -0
- package/dist/ui/colors.d.ts +4 -0
- package/dist/ui/colors.js +7 -0
- package/dist/ui/keyboard.d.ts +12 -0
- package/dist/ui/keyboard.js +57 -0
- package/dist/ui/status-line.d.ts +6 -0
- package/dist/ui/status-line.js +53 -0
- package/dist/utils/__tests__/output.test.d.ts +1 -0
- package/dist/utils/__tests__/output.test.js +38 -0
- package/dist/utils/__tests__/prompt.test.js +23 -99
- package/dist/utils/output.d.ts +17 -0
- package/dist/utils/output.js +27 -0
- package/dist/utils/prompt.d.ts +1 -0
- package/dist/utils/prompt.js +29 -21
- package/package.json +4 -2
package/dist/api/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Credentials, AppInfo, WorkspaceInfo, DevSession } from '../types.js';
|
|
1
|
+
import type { Credentials, AppInfo, WorkspaceInfo, DevSession, WorkspaceAllData } from '../types.js';
|
|
2
2
|
export declare class ApiClient {
|
|
3
3
|
private baseUrl;
|
|
4
4
|
private apiKey;
|
|
@@ -90,5 +90,6 @@ export declare class ApiClient {
|
|
|
90
90
|
* Both canonical ({baseUrl}/api/git/{workspaceId}/{appId}) and
|
|
91
91
|
* subdomain ({slug}.runwork.ai/api/git/{appId}) formats are supported.
|
|
92
92
|
*/
|
|
93
|
+
getWorkspaceAll(workspaceId: string): Promise<WorkspaceAllData>;
|
|
93
94
|
getGitRemoteUrl(workspaceId: string, appId: string): string;
|
|
94
95
|
}
|
package/dist/api/client.js
CHANGED
|
@@ -147,6 +147,10 @@ export class ApiClient {
|
|
|
147
147
|
* Both canonical ({baseUrl}/api/git/{workspaceId}/{appId}) and
|
|
148
148
|
* subdomain ({slug}.runwork.ai/api/git/{appId}) formats are supported.
|
|
149
149
|
*/
|
|
150
|
+
async getWorkspaceAll(workspaceId) {
|
|
151
|
+
const res = await this.request(`/api/workspaces/${workspaceId}/all`);
|
|
152
|
+
return res.data;
|
|
153
|
+
}
|
|
150
154
|
getGitRemoteUrl(workspaceId, appId) {
|
|
151
155
|
return `${this.baseUrl}/api/git/${workspaceId}/${appId}`;
|
|
152
156
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Credentials } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Performs the full browser-based login flow:
|
|
4
|
+
* 1. Initiates login, opens browser
|
|
5
|
+
* 2. Polls for completion
|
|
6
|
+
* 3. Saves credentials and configures git
|
|
7
|
+
*
|
|
8
|
+
* Returns the credentials on success, exits process on timeout.
|
|
9
|
+
*/
|
|
10
|
+
export declare function performLogin(baseUrl?: string): Promise<Credentials>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ApiClient } from '../api/client.js';
|
|
2
|
+
import { saveCredentials } from './store.js';
|
|
3
|
+
import { configureGitCredentials } from '../git/credentials.js';
|
|
4
|
+
const DEFAULT_BASE_URL = 'https://runwork.ai';
|
|
5
|
+
/**
|
|
6
|
+
* Performs the full browser-based login flow:
|
|
7
|
+
* 1. Initiates login, opens browser
|
|
8
|
+
* 2. Polls for completion
|
|
9
|
+
* 3. Saves credentials and configures git
|
|
10
|
+
*
|
|
11
|
+
* Returns the credentials on success, exits process on timeout.
|
|
12
|
+
*/
|
|
13
|
+
export async function performLogin(baseUrl) {
|
|
14
|
+
const url = baseUrl || DEFAULT_BASE_URL;
|
|
15
|
+
const client = new ApiClient({ apiKey: '', email: '', baseUrl: url });
|
|
16
|
+
const { sessionId, loginUrl } = await client.initiateLogin();
|
|
17
|
+
// Open browser
|
|
18
|
+
const open = await import('open');
|
|
19
|
+
await open.default(loginUrl);
|
|
20
|
+
console.log(`If browser didn't open, visit: ${loginUrl}`);
|
|
21
|
+
console.log('');
|
|
22
|
+
console.log('Waiting for authentication...');
|
|
23
|
+
const maxAttempts = 60;
|
|
24
|
+
const pollInterval = 5000;
|
|
25
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
26
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
27
|
+
const result = await client.pollLogin(sessionId);
|
|
28
|
+
if (result) {
|
|
29
|
+
saveCredentials(result);
|
|
30
|
+
await configureGitCredentials(result.baseUrl || url);
|
|
31
|
+
console.log(`Logged in as ${result.email}`);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
console.error('Login timed out. Please try again.');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
package/dist/commands/clone.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { ApiClient } from '../api/client.js';
|
|
3
|
+
import type { AppInfo } from '../types.js';
|
|
4
|
+
export declare function execClone(client: ApiClient, app: AppInfo, directory?: string): Promise<string>;
|
|
2
5
|
export declare const cloneCommand: Command;
|
package/dist/commands/clone.js
CHANGED
|
@@ -1,37 +1,18 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { execFileSync } from 'child_process';
|
|
3
3
|
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
4
|
+
import { join, resolve } from 'path';
|
|
5
5
|
import { requireAuth } from '../auth/store.js';
|
|
6
6
|
import { ApiClient } from '../api/client.js';
|
|
7
7
|
import { promptSelect } from '../utils/prompt.js';
|
|
8
8
|
import { generateManifest, saveManifest } from '../template/manifest.js';
|
|
9
9
|
import { extractZip } from '../utils/zip.js';
|
|
10
10
|
import { removeNestedGitDirs } from '../utils/fs.js';
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
}
|
|
11
|
+
import { runAgentWizard } from '../ui/banner.js';
|
|
12
|
+
export async function execClone(client, app, directory) {
|
|
31
13
|
const slug = app.slug || app.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
32
14
|
const dir = directory || slug;
|
|
33
15
|
const remoteUrl = client.getGitRemoteUrl(app.workspaceId, app.id);
|
|
34
|
-
console.log(`Cloning "${app.name}"...`);
|
|
35
16
|
// Step 1: Extract skeleton template as the base layer
|
|
36
17
|
console.log('Downloading project template...');
|
|
37
18
|
mkdirSync(dir, { recursive: true });
|
|
@@ -70,6 +51,7 @@ export const cloneCommand = new Command('clone')
|
|
|
70
51
|
// Write .runwork.json config
|
|
71
52
|
const config = {
|
|
72
53
|
workspaceId: app.workspaceId,
|
|
54
|
+
workspaceName: app.workspaceName,
|
|
73
55
|
appId: app.id,
|
|
74
56
|
appName: app.name,
|
|
75
57
|
};
|
|
@@ -85,6 +67,32 @@ export const cloneCommand = new Command('clone')
|
|
|
85
67
|
// App may be new with no registries yet
|
|
86
68
|
}
|
|
87
69
|
console.log(`\nApp "${app.name}" cloned to ${dir}/`);
|
|
88
|
-
console.log(`Remote: ${
|
|
89
|
-
|
|
70
|
+
console.log(`Remote: ${client.getGitRemoteUrl(app.workspaceId, app.id)}`);
|
|
71
|
+
return resolve(dir);
|
|
72
|
+
}
|
|
73
|
+
export const cloneCommand = new Command('clone')
|
|
74
|
+
.description('Clone a Runwork app to local development')
|
|
75
|
+
.argument('[appId]', 'App ID to clone (interactive if omitted)')
|
|
76
|
+
.argument('[directory]', 'Target directory')
|
|
77
|
+
.action(async (appId, directory) => {
|
|
78
|
+
const creds = requireAuth();
|
|
79
|
+
const client = new ApiClient(creds);
|
|
80
|
+
let app;
|
|
81
|
+
if (appId) {
|
|
82
|
+
app = await client.getApp(appId);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const apps = await client.listApps();
|
|
86
|
+
if (apps.length === 0) {
|
|
87
|
+
console.error('No apps found.');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const choice = await promptSelect('Select app to clone:', apps.map(a => ({ label: `${a.name} (${a.workspaceName})`, value: a })));
|
|
91
|
+
app = choice.value;
|
|
92
|
+
}
|
|
93
|
+
console.log(`Cloning "${app.name}"...`);
|
|
94
|
+
const dir = await execClone(client, app, directory);
|
|
95
|
+
const slug = dir.split('/').pop() || dir;
|
|
96
|
+
await runAgentWizard(dir);
|
|
97
|
+
console.log(`Next: cd ${slug} && runwork dev`);
|
|
90
98
|
});
|
package/dist/commands/deploy.js
CHANGED
|
@@ -3,10 +3,16 @@ import { execFileSync } from 'child_process';
|
|
|
3
3
|
import { readFileSync, existsSync } from 'fs';
|
|
4
4
|
import { requireAuth } from '../auth/store.js';
|
|
5
5
|
import { ApiClient } from '../api/client.js';
|
|
6
|
+
import { shouldOutputJson, jsonOut } from '../utils/output.js';
|
|
6
7
|
export const deployCommand = new Command('deploy')
|
|
7
|
-
.description('Deploy the current app to
|
|
8
|
-
.action(async () => {
|
|
8
|
+
.description('Deploy the current app to production')
|
|
9
|
+
.action(async (_opts, command) => {
|
|
10
|
+
const useJson = shouldOutputJson(command.optsWithGlobals().json);
|
|
9
11
|
if (!existsSync('.runwork.json')) {
|
|
12
|
+
if (useJson) {
|
|
13
|
+
process.stderr.write(JSON.stringify({ error: 'No .runwork.json found. Run `runwork init` first.' }) + '\n');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
10
16
|
console.error('No .runwork.json found. Run `runwork init` first.');
|
|
11
17
|
process.exit(1);
|
|
12
18
|
}
|
|
@@ -23,21 +29,32 @@ export const deployCommand = new Command('deploy')
|
|
|
23
29
|
// No commits yet
|
|
24
30
|
}
|
|
25
31
|
if (repoHasCommits) {
|
|
26
|
-
|
|
32
|
+
if (!useJson)
|
|
33
|
+
console.log('Syncing...');
|
|
27
34
|
try {
|
|
28
|
-
execFileSync('git', ['pull', '--rebase', 'runwork', 'main'], { stdio: 'inherit' });
|
|
35
|
+
execFileSync('git', ['pull', '--rebase', 'runwork', 'main'], { stdio: useJson ? 'pipe' : 'inherit' });
|
|
29
36
|
}
|
|
30
37
|
catch {
|
|
31
|
-
|
|
38
|
+
if (!useJson)
|
|
39
|
+
console.warn('Pull failed. Continuing...');
|
|
32
40
|
}
|
|
33
|
-
execFileSync('git', ['push', 'runwork', 'main'], { stdio: 'inherit' });
|
|
41
|
+
execFileSync('git', ['push', 'runwork', 'main'], { stdio: useJson ? 'pipe' : 'inherit' });
|
|
34
42
|
}
|
|
35
43
|
else {
|
|
44
|
+
if (useJson) {
|
|
45
|
+
process.stderr.write(JSON.stringify({ error: 'No commits to deploy. Commit your changes first.' }) + '\n');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
36
48
|
console.error('No commits to deploy. Commit your changes first.');
|
|
37
49
|
process.exit(1);
|
|
38
50
|
}
|
|
39
51
|
// Trigger deploy
|
|
40
|
-
|
|
52
|
+
if (!useJson)
|
|
53
|
+
console.log('Deploying...');
|
|
41
54
|
const result = await client.triggerDeploy(config.appId);
|
|
55
|
+
if (useJson) {
|
|
56
|
+
jsonOut({ deployed: true, url: result.deploymentUrl });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
42
59
|
console.log(`Deployed: ${result.deploymentUrl}`);
|
|
43
60
|
});
|
package/dist/commands/dev.d.ts
CHANGED
package/dist/commands/dev.js
CHANGED
|
@@ -11,6 +11,11 @@ import { populateTypes } from '../types-manager.js';
|
|
|
11
11
|
import { loadManifest, generateManifest, saveManifest, detectUserEdits } from '../template/manifest.js';
|
|
12
12
|
import { extractZip } from '../utils/zip.js';
|
|
13
13
|
import { removeNestedGitDirs } from '../utils/fs.js';
|
|
14
|
+
import { getDevBanner, getKeyboardHints, getInfoPanel } from '../ui/banner.js';
|
|
15
|
+
import { createStatusLine } from '../ui/status-line.js';
|
|
16
|
+
import { createKeyboardListener } from '../ui/keyboard.js';
|
|
17
|
+
import { bold, dim, green, yellow, cyan } from '../ui/colors.js';
|
|
18
|
+
import { shouldOutputJson, jsonLine } from '../utils/output.js';
|
|
14
19
|
async function populateSkill(projectDir, client, appId) {
|
|
15
20
|
try {
|
|
16
21
|
const { skill } = await client.getAppSkill(appId);
|
|
@@ -29,23 +34,26 @@ function readConfig() {
|
|
|
29
34
|
}
|
|
30
35
|
return JSON.parse(readFileSync('.runwork.json', 'utf-8'));
|
|
31
36
|
}
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
.option('--no-logs', 'Disable automatic log tailing')
|
|
35
|
-
.option('--logs-only-file', 'Write logs to file only, not terminal')
|
|
36
|
-
.action(async (options) => {
|
|
37
|
+
export async function execDev(options) {
|
|
38
|
+
const useJson = options?.json ?? false;
|
|
37
39
|
const config = readConfig();
|
|
38
40
|
const creds = requireAuth();
|
|
39
41
|
const client = new ApiClient(creds);
|
|
40
42
|
const cwd = process.cwd();
|
|
43
|
+
const ts = () => new Date().toISOString();
|
|
41
44
|
// Detect user edits made outside of `runwork dev`
|
|
42
45
|
const oldManifest = await loadManifest(cwd);
|
|
43
46
|
if (oldManifest) {
|
|
44
47
|
const userEdits = await detectUserEdits(cwd, oldManifest);
|
|
45
48
|
if (userEdits.length > 0) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
if (useJson) {
|
|
50
|
+
jsonLine({ event: 'startup', phase: 'user_edits_detected', files: userEdits, timestamp: ts() });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log(`Detected ${userEdits.length} file(s) edited outside dev session:`);
|
|
54
|
+
for (const file of userEdits) {
|
|
55
|
+
console.log(` ${file}`);
|
|
56
|
+
}
|
|
49
57
|
}
|
|
50
58
|
try {
|
|
51
59
|
execFileSync('git', ['add', '--', ...userEdits], { stdio: 'pipe' });
|
|
@@ -57,15 +65,18 @@ export const devCommand = new Command('dev')
|
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
67
|
// Download fresh template to ensure latest version
|
|
60
|
-
|
|
68
|
+
if (useJson) {
|
|
69
|
+
jsonLine({ event: 'startup', phase: 'template_update', timestamp: ts() });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log(dim('Updating template...'));
|
|
73
|
+
}
|
|
61
74
|
try {
|
|
62
75
|
const zipData = await client.downloadSkeleton();
|
|
63
76
|
extractZip(zipData, cwd);
|
|
64
77
|
removeNestedGitDirs(cwd);
|
|
65
|
-
// Generate new manifest from fresh template (before restoring user files)
|
|
66
78
|
const newManifest = await generateManifest(cwd);
|
|
67
79
|
await saveManifest(cwd, newManifest);
|
|
68
|
-
// Restore user-edited files from git on top of fresh template
|
|
69
80
|
try {
|
|
70
81
|
execFileSync('git', ['checkout', '--', '.'], { cwd, stdio: 'pipe' });
|
|
71
82
|
}
|
|
@@ -74,65 +85,160 @@ export const devCommand = new Command('dev')
|
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
catch {
|
|
77
|
-
|
|
88
|
+
if (!useJson)
|
|
89
|
+
console.warn(yellow('Template update failed. Continuing with current files.'));
|
|
78
90
|
}
|
|
79
91
|
// Populate type definitions
|
|
80
92
|
await populateTypes(cwd);
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
if (useJson)
|
|
94
|
+
jsonLine({ event: 'startup', phase: 'types_populated', timestamp: ts() });
|
|
95
|
+
// Start dev session
|
|
96
|
+
if (useJson) {
|
|
97
|
+
jsonLine({ event: 'startup', phase: 'dev_session', timestamp: ts() });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.log(dim('Starting dev session...'));
|
|
101
|
+
}
|
|
83
102
|
const session = await client.startDevSession(config.appId);
|
|
84
|
-
|
|
85
|
-
// Fetch SKILL.md (best-effort, after session so DO registries are available)
|
|
103
|
+
// Fetch SKILL.md (best-effort)
|
|
86
104
|
await populateSkill(cwd, client, config.appId);
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
if (useJson)
|
|
106
|
+
jsonLine({ event: 'startup', phase: 'skill_fetched', timestamp: ts() });
|
|
107
|
+
// Sync
|
|
108
|
+
if (!useJson)
|
|
109
|
+
console.log(dim('Syncing...'));
|
|
89
110
|
const syncResult = syncWithRemote(cwd);
|
|
90
|
-
|
|
91
|
-
|
|
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') {
|
|
102
|
-
console.error('Merge conflicts detected after stash pop. Resolve conflicts and restart.');
|
|
103
|
-
process.exit(1);
|
|
104
|
-
}
|
|
105
|
-
console.warn('Pull failed (remote may not have commits yet). Continuing...');
|
|
106
|
-
break;
|
|
111
|
+
if (useJson) {
|
|
112
|
+
jsonLine({ event: 'startup', phase: 'sync', status: syncResult.status, pushed: syncResult.pushed, timestamp: ts() });
|
|
107
113
|
}
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
else {
|
|
115
|
+
switch (syncResult.status) {
|
|
116
|
+
case 'skipped':
|
|
117
|
+
console.log(dim(' No commits yet. Skipping sync.'));
|
|
118
|
+
break;
|
|
119
|
+
case 'synced':
|
|
120
|
+
console.log(green(' Synced with Runwork.'));
|
|
121
|
+
break;
|
|
122
|
+
case 'merged':
|
|
123
|
+
console.log(yellow(' Merged with Runwork (histories diverged).'));
|
|
124
|
+
break;
|
|
125
|
+
case 'sync-failed':
|
|
126
|
+
if (syncResult.error === 'stash-conflict') {
|
|
127
|
+
console.error('Merge conflicts detected after stash pop. Resolve conflicts and restart.');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
console.warn(yellow(`Sync failed. Continuing...`));
|
|
131
|
+
if (syncResult.error) {
|
|
132
|
+
console.warn(dim(` ${syncResult.error}`));
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
if (!syncResult.pushed && syncResult.status !== 'skipped' && syncResult.status !== 'sync-failed') {
|
|
137
|
+
console.warn(yellow('Push failed. Continuing with current state...'));
|
|
138
|
+
}
|
|
110
139
|
}
|
|
111
|
-
//
|
|
140
|
+
// Emit session_started (JSON) or show banner (human)
|
|
141
|
+
if (useJson) {
|
|
142
|
+
jsonLine({ event: 'session_started', previewUrl: session.previewUrl, appName: config.appName, timestamp: ts() });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.log(getDevBanner({ appName: config.appName, previewUrl: session.previewUrl, workspaceName: config.workspaceName }));
|
|
146
|
+
console.log(getKeyboardHints());
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log(dim(' ─────────────────────────────────────────────'));
|
|
149
|
+
console.log('');
|
|
150
|
+
}
|
|
151
|
+
// Status line and keyboard only in human mode
|
|
152
|
+
const statusLine = useJson ? null : createStatusLine(process.stdout);
|
|
153
|
+
let syncedCount = 0;
|
|
154
|
+
let logFilter = 'all';
|
|
155
|
+
function updateStatus() {
|
|
156
|
+
if (!statusLine)
|
|
157
|
+
return;
|
|
158
|
+
const filterLabel = logFilter === 'all' ? 'All' : logFilter === 'events' ? 'Events' : 'Runtime';
|
|
159
|
+
const sep = '\x1b[90m\u2502\x1b[39m';
|
|
160
|
+
statusLine.update(`${bold(config.appName)} ${sep} ${green(session.previewUrl)} ${sep} ${cyan(`${syncedCount} synced`)} ${sep} ${filterLabel} ${sep} \x1b[90mo\x1b[39m:open \x1b[90mi\x1b[39m:info \x1b[90ma/e/r\x1b[39m:filter \x1b[90mq\x1b[39m:quit`);
|
|
161
|
+
}
|
|
162
|
+
if (statusLine)
|
|
163
|
+
updateStatus();
|
|
112
164
|
let logTailer;
|
|
113
|
-
|
|
165
|
+
const keyboard = useJson ? null : createKeyboardListener();
|
|
114
166
|
const cleanup = async () => {
|
|
115
|
-
|
|
167
|
+
if (!useJson)
|
|
168
|
+
console.log('\nStopping...');
|
|
116
169
|
logTailer?.stop();
|
|
117
170
|
await stopAutoCommit();
|
|
171
|
+
statusLine?.destroy();
|
|
172
|
+
keyboard?.stop();
|
|
118
173
|
process.exit(0);
|
|
119
174
|
};
|
|
120
175
|
process.on('SIGINT', cleanup);
|
|
121
176
|
process.on('SIGTERM', cleanup);
|
|
177
|
+
if (keyboard) {
|
|
178
|
+
keyboard.start((action) => {
|
|
179
|
+
switch (action) {
|
|
180
|
+
case 'o':
|
|
181
|
+
case 'p':
|
|
182
|
+
import('open').then(m => m.default(session.previewUrl));
|
|
183
|
+
break;
|
|
184
|
+
case 'a':
|
|
185
|
+
logFilter = 'all';
|
|
186
|
+
console.log(dim(' Showing all logs.'));
|
|
187
|
+
updateStatus();
|
|
188
|
+
break;
|
|
189
|
+
case 'e':
|
|
190
|
+
logFilter = 'events';
|
|
191
|
+
console.log(dim(' Showing events only.'));
|
|
192
|
+
updateStatus();
|
|
193
|
+
break;
|
|
194
|
+
case 'r':
|
|
195
|
+
logFilter = 'runtime';
|
|
196
|
+
console.log(dim(' Showing runtime logs only.'));
|
|
197
|
+
updateStatus();
|
|
198
|
+
break;
|
|
199
|
+
case 'i':
|
|
200
|
+
console.log(getInfoPanel({
|
|
201
|
+
appName: config.appName,
|
|
202
|
+
previewUrl: session.previewUrl,
|
|
203
|
+
workspaceName: config.workspaceName,
|
|
204
|
+
directory: cwd,
|
|
205
|
+
}));
|
|
206
|
+
break;
|
|
207
|
+
case 'quit':
|
|
208
|
+
cleanup();
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
122
213
|
// Start file watcher
|
|
123
|
-
|
|
124
|
-
|
|
214
|
+
await watchAndAutoCommit(process.cwd(), client, config.appId, {
|
|
215
|
+
onFastSync: useJson
|
|
216
|
+
? (count) => { jsonLine({ event: 'files_synced', count, target: 'preview', timestamp: ts() }); }
|
|
217
|
+
: (count) => { syncedCount += count; updateStatus(); },
|
|
218
|
+
onGitPush: useJson
|
|
219
|
+
? (count) => { jsonLine({ event: 'files_pushed', count, target: 'git', timestamp: ts() }); }
|
|
220
|
+
: (count) => { syncedCount += count; updateStatus(); },
|
|
221
|
+
});
|
|
125
222
|
// Start log tailing (unless --no-logs)
|
|
126
|
-
if (options
|
|
223
|
+
if (options?.logs !== false) {
|
|
127
224
|
logTailer = startLogTailer({
|
|
128
225
|
appId: config.appId,
|
|
129
226
|
client,
|
|
130
227
|
projectDir: cwd,
|
|
131
|
-
toTerminal: !options
|
|
228
|
+
toTerminal: !options?.logsOnlyFile,
|
|
132
229
|
toFile: true,
|
|
230
|
+
getFilter: () => logFilter,
|
|
231
|
+
useJson,
|
|
133
232
|
});
|
|
134
|
-
console.log('Log tailing active. Logs written to .runwork/logs.txt');
|
|
135
233
|
}
|
|
136
234
|
// Keep alive
|
|
137
235
|
await new Promise(() => { });
|
|
236
|
+
}
|
|
237
|
+
export const devCommand = new Command('dev')
|
|
238
|
+
.description('Start local development with live sync, preview sandbox, and file watching')
|
|
239
|
+
.option('--no-logs', 'Disable automatic log tailing')
|
|
240
|
+
.option('--logs-only-file', 'Write logs to file only, not terminal')
|
|
241
|
+
.action(async (options, command) => {
|
|
242
|
+
const globalJson = shouldOutputJson(command.optsWithGlobals().json);
|
|
243
|
+
await execDev({ ...options, json: globalJson });
|
|
138
244
|
});
|