runwork 0.13.3 → 0.14.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/bundled-types/core-endpoints.d.ts +16 -4
- package/bundled-types/core-workflow-instance.d.ts +6 -0
- package/bundled-types/workflows.d.ts +1 -0
- package/dist/agents/claude-desktop-plugin-tree.js +23 -8
- package/dist/agents/claude-desktop.js +6 -3
- package/dist/agents/codex.js +15 -7
- package/dist/api/client.d.ts +5 -0
- package/dist/api/client.js +8 -0
- package/dist/commands/__tests__/mcp-entries.test.d.ts +1 -0
- package/dist/commands/__tests__/mcp-entries.test.js +48 -0
- package/dist/commands/deploy.js +101 -22
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.js +145 -155
- package/dist/commands/logs.js +10 -3
- package/dist/commands/mcp-entries.d.ts +13 -0
- package/dist/commands/mcp-entries.js +26 -0
- package/dist/commands/sync.js +6 -10
- package/dist/dev/__tests__/session.test.js +60 -0
- package/dist/dev/session.d.ts +8 -0
- package/dist/dev/session.js +4 -1
- package/dist/generated/bundled-types.js +3 -3
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/git/__tests__/auto-commit.test.js +59 -1
- package/dist/git/__tests__/deploy-guard.test.d.ts +1 -0
- package/dist/git/__tests__/deploy-guard.test.js +61 -0
- package/dist/git/auto-commit.d.ts +22 -0
- package/dist/git/auto-commit.js +72 -11
- package/dist/git/critical-files.d.ts +20 -0
- package/dist/git/critical-files.js +68 -0
- package/dist/git/deploy-guard.d.ts +53 -0
- package/dist/git/deploy-guard.js +78 -0
- package/dist/types.d.ts +3 -0
- package/dist/utils/agent-guidance.d.ts +2 -0
- package/dist/utils/ignore-matcher.d.ts +9 -0
- package/dist/utils/ignore-matcher.js +16 -0
- package/package.json +1 -1
|
@@ -10,9 +10,16 @@ import { z } from 'zod';
|
|
|
10
10
|
import type { Hono } from 'hono';
|
|
11
11
|
type ZodSchema = z.ZodType<any, any, any>;
|
|
12
12
|
/**
|
|
13
|
-
* Authentication
|
|
13
|
+
* Authentication requirement declared by an endpoint definition.
|
|
14
14
|
*/
|
|
15
15
|
export type EndpointAuthType = 'apiKey' | 'public';
|
|
16
|
+
/**
|
|
17
|
+
* Caller principal type resolved by the platform and passed via the
|
|
18
|
+
* `X-Public-Endpoint-Auth-Type` header. `user` and `workspace_key` are
|
|
19
|
+
* authenticated principals the platform has already authorized; `api_key`
|
|
20
|
+
* additionally carries a validated key id; `none` is unauthenticated.
|
|
21
|
+
*/
|
|
22
|
+
export type EndpointPrincipalType = 'none' | 'api_key' | 'workspace_key' | 'user';
|
|
16
23
|
/**
|
|
17
24
|
* HTTP methods supported by endpoints
|
|
18
25
|
*/
|
|
@@ -53,10 +60,14 @@ export interface EndpointContext<TQuery = Record<string, unknown>, TBody = unkno
|
|
|
53
60
|
* Authentication information passed to handlers
|
|
54
61
|
*/
|
|
55
62
|
export interface EndpointAuthInfo {
|
|
56
|
-
/** Authentication
|
|
63
|
+
/** Authentication requirement declared by the endpoint */
|
|
57
64
|
type: EndpointAuthType;
|
|
58
|
-
/**
|
|
65
|
+
/** Resolved caller principal type (api_key, workspace_key, user, none) */
|
|
66
|
+
principalType?: EndpointPrincipalType;
|
|
67
|
+
/** API key ID (if authenticated with an API key) */
|
|
59
68
|
apiKeyId?: string;
|
|
69
|
+
/** User ID (if authenticated as a workspace member) */
|
|
70
|
+
userId?: string;
|
|
60
71
|
/** Scopes granted to the API key */
|
|
61
72
|
scopes?: string[];
|
|
62
73
|
/** Whether request is authenticated */
|
|
@@ -112,8 +123,9 @@ export declare function isEndpointDefinition(entry: unknown): entry is EndpointD
|
|
|
112
123
|
export interface ParsedEndpointRequest {
|
|
113
124
|
endpointId: string;
|
|
114
125
|
appId: string;
|
|
115
|
-
authType:
|
|
126
|
+
authType: EndpointPrincipalType;
|
|
116
127
|
apiKeyId?: string;
|
|
128
|
+
userId?: string;
|
|
117
129
|
scopes?: string[];
|
|
118
130
|
}
|
|
119
131
|
/**
|
|
@@ -9,6 +9,12 @@ import { DurableObject } from 'cloudflare:workers';
|
|
|
9
9
|
import type { NativeWorkflowInstance, NativeWorkflowConfig } from './core-workflow-types';
|
|
10
10
|
import type { StepOptions, WaitEventOptions } from './core-workflows';
|
|
11
11
|
import { type Env } from './core-utils';
|
|
12
|
+
export declare class WorkflowPausedError extends Error {
|
|
13
|
+
reason: 'sleep' | 'waitEvent';
|
|
14
|
+
resumeAt?: number | undefined;
|
|
15
|
+
constructor(reason: 'sleep' | 'waitEvent', resumeAt?: number | undefined);
|
|
16
|
+
}
|
|
17
|
+
export declare function isWorkflowControlSignal(error: unknown): boolean;
|
|
12
18
|
/**
|
|
13
19
|
* WorkflowInstance Durable Object
|
|
14
20
|
*
|
|
@@ -12,3 +12,4 @@ export { DEFAULT_WORKFLOW_CONFIG } from './core-workflow-types';
|
|
|
12
12
|
export { WORKFLOW_INFRA_MODE } from './core-workflow-config';
|
|
13
13
|
export { WorkflowInstanceDO } from './core-workflow-instance';
|
|
14
14
|
export { WorkflowCoordinator } from './core-workflow-coordinator';
|
|
15
|
+
export { WorkflowPausedError, isWorkflowControlSignal } from './core-workflow-instance';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chmodSync, mkdirSync, rmSync, existsSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { appendTokenToUrl, buildSkillMd } from './types.js';
|
|
3
|
+
import { appendTokenToUrl, buildSkillMd, RUNWORK_WORKSPACE_MCP_NAME } from './types.js';
|
|
4
4
|
import { SESSION_START_HOOK_SCRIPT } from './session-start-hook.js';
|
|
5
5
|
/**
|
|
6
6
|
* Write <destDir>/.claude-plugin/plugin.json.
|
|
@@ -24,13 +24,28 @@ export function writePluginMcpConfig(destDir, mcpServers) {
|
|
|
24
24
|
mkdirSync(destDir, { recursive: true });
|
|
25
25
|
const mcpEntries = {};
|
|
26
26
|
for (const server of mcpServers) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
if (server.name === RUNWORK_WORKSPACE_MCP_NAME) {
|
|
28
|
+
// Preserve the exact, known-working behavior for the Runwork connector: embed
|
|
29
|
+
// the workspace key in the URL, no headers field. Claude Desktop's native
|
|
30
|
+
// connector did not reliably honor request headers here, which is why the token
|
|
31
|
+
// lives in the URL — do NOT change this.
|
|
32
|
+
const token = server.headers?.Authorization?.replace(/^Bearer\s+/i, '');
|
|
33
|
+
mcpEntries[server.name] = {
|
|
34
|
+
type: 'http',
|
|
35
|
+
url: token ? appendTokenToUrl(server.url, token) : server.url,
|
|
36
|
+
...(server.description ? { description: server.description } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Third-party servers: forward the auth header map (same .mcp.json schema
|
|
41
|
+
// claude-code uses). Their secrets must NEVER be placed in the URL.
|
|
42
|
+
mcpEntries[server.name] = {
|
|
43
|
+
type: 'http',
|
|
44
|
+
url: server.url,
|
|
45
|
+
...(server.headers ? { headers: server.headers } : {}),
|
|
46
|
+
...(server.description ? { description: server.description } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
34
49
|
}
|
|
35
50
|
writeFileSync(join(destDir, '.mcp.json'), JSON.stringify({ mcpServers: mcpEntries }, null, 2));
|
|
36
51
|
}
|
|
@@ -189,12 +189,15 @@ export class ClaudeDesktopAdapter {
|
|
|
189
189
|
// For cowork connectors, embed the auth token in the URL query string.
|
|
190
190
|
// This lets Claude Desktop connect without requiring the user to go through
|
|
191
191
|
// an OAuth flow or manually click "Connect" in the Connectors UI.
|
|
192
|
-
// Write to claude_desktop_config.json via mcp-remote (chat mode fallback)
|
|
192
|
+
// Write to claude_desktop_config.json via mcp-remote (chat mode fallback).
|
|
193
|
+
// Forward every auth header (Authorization, X-Api-Key, etc.) as a --header arg.
|
|
193
194
|
const entries = {};
|
|
194
195
|
for (const s of servers) {
|
|
195
196
|
const args = ['mcp-remote', s.url];
|
|
196
|
-
if (s.headers
|
|
197
|
-
|
|
197
|
+
if (s.headers) {
|
|
198
|
+
for (const [name, value] of Object.entries(s.headers)) {
|
|
199
|
+
args.push('--header', `${name}:${value}`);
|
|
200
|
+
}
|
|
198
201
|
}
|
|
199
202
|
entries[s.name] = {
|
|
200
203
|
command: 'npx',
|
package/dist/agents/codex.js
CHANGED
|
@@ -45,15 +45,23 @@ export class CodexAdapter {
|
|
|
45
45
|
delete mcpServers[key];
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
// Add new entries (sanitize names to match Codex's ^[a-zA-Z0-9_-]+$ requirement)
|
|
48
|
+
// Add new entries (sanitize names to match Codex's ^[a-zA-Z0-9_-]+$ requirement).
|
|
49
49
|
for (const s of servers) {
|
|
50
50
|
const safeName = s.name.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
const entry = { url: s.url };
|
|
52
|
+
if (s.name === RUNWORK_WORKSPACE_MCP_NAME) {
|
|
53
|
+
// Preserve the known-working behavior: embed the workspace key in the URL.
|
|
54
|
+
// Codex's header-based auth was historically unreliable here, so do NOT change it.
|
|
55
|
+
const token = s.headers?.Authorization?.replace(/^Bearer\s+/i, '');
|
|
56
|
+
if (token)
|
|
57
|
+
entry.url = appendTokenToUrl(s.url, token);
|
|
58
|
+
}
|
|
59
|
+
else if (s.headers && Object.keys(s.headers).length > 0) {
|
|
60
|
+
// External servers: forward headers via Codex's documented http_headers field
|
|
61
|
+
// (static literal values). These had no prior working path, so this is additive.
|
|
62
|
+
entry.http_headers = s.headers;
|
|
63
|
+
}
|
|
64
|
+
mcpServers[safeName] = entry;
|
|
57
65
|
}
|
|
58
66
|
mkdirSync(join(configPath, '..'), { recursive: true });
|
|
59
67
|
writeFileSync(configPath, stringify(parsed));
|
package/dist/api/client.d.ts
CHANGED
|
@@ -141,6 +141,11 @@ export declare class ApiClient {
|
|
|
141
141
|
transport: 'sse' | 'streamable-http';
|
|
142
142
|
}): Promise<McpServerConfig>;
|
|
143
143
|
removeMcpServer(workspaceId: string, serverId: string): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* Resolve decrypted auth headers for header-auth MCP servers, scoped to the
|
|
146
|
+
* authenticated user (their own + workspace-wide credentials). Keyed by server id.
|
|
147
|
+
*/
|
|
148
|
+
resolveMcpCredentials(workspaceId: string): Promise<Record<string, Record<string, string>>>;
|
|
144
149
|
searchCommunitySkills(query: string, limit?: number, includeContent?: boolean): Promise<{
|
|
145
150
|
query: string;
|
|
146
151
|
count: number;
|
package/dist/api/client.js
CHANGED
|
@@ -194,6 +194,14 @@ export class ApiClient {
|
|
|
194
194
|
async removeMcpServer(workspaceId, serverId) {
|
|
195
195
|
await this.request(`/api/workspaces/${workspaceId}/mcp-servers/${serverId}`, { method: 'DELETE' });
|
|
196
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Resolve decrypted auth headers for header-auth MCP servers, scoped to the
|
|
199
|
+
* authenticated user (their own + workspace-wide credentials). Keyed by server id.
|
|
200
|
+
*/
|
|
201
|
+
async resolveMcpCredentials(workspaceId) {
|
|
202
|
+
const res = await this.request(`/api/workspaces/${workspaceId}/mcp-credentials/resolved`);
|
|
203
|
+
return res.data.servers;
|
|
204
|
+
}
|
|
197
205
|
// --- Community Marketplace ---
|
|
198
206
|
async searchCommunitySkills(query, limit = 10, includeContent = false) {
|
|
199
207
|
const res = await this.request(`/api/community/skills/search?q=${encodeURIComponent(query)}&limit=${limit}&include_content=${includeContent}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildExternalMcpEntries } from '../mcp-entries.js';
|
|
3
|
+
const PLATFORM_KEY = 'rk_ws_live_supersecretplatformkey';
|
|
4
|
+
function server(overrides) {
|
|
5
|
+
return {
|
|
6
|
+
id: 'srv-1',
|
|
7
|
+
name: 'example',
|
|
8
|
+
url: 'https://mcp.example.com',
|
|
9
|
+
transport: 'streamable-http',
|
|
10
|
+
enabled: true,
|
|
11
|
+
addedAt: 0,
|
|
12
|
+
addedBy: 'user-1',
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
describe('buildExternalMcpEntries', () => {
|
|
17
|
+
it('never injects the Runwork platform key onto external entries', () => {
|
|
18
|
+
const entries = buildExternalMcpEntries([
|
|
19
|
+
server({ id: 'a', authType: 'header' }),
|
|
20
|
+
server({ id: 'b', authType: 'oauth' }),
|
|
21
|
+
server({ id: 'c', authType: 'none' }),
|
|
22
|
+
server({ id: 'd' }), // authType unset
|
|
23
|
+
], { a: { Authorization: 'Bearer user-token' } });
|
|
24
|
+
// Regression: the platform key must not appear anywhere in the output.
|
|
25
|
+
expect(JSON.stringify(entries)).not.toContain(PLATFORM_KEY);
|
|
26
|
+
});
|
|
27
|
+
it('injects resolved per-server headers for header-auth servers', () => {
|
|
28
|
+
const entries = buildExternalMcpEntries([server({ id: 'a', authType: 'header' })], { a: { Authorization: 'Bearer tok', 'X-Api-Key': 'key123' } });
|
|
29
|
+
expect(entries[0].headers).toEqual({ Authorization: 'Bearer tok', 'X-Api-Key': 'key123' });
|
|
30
|
+
});
|
|
31
|
+
it('emits no auth header for oauth servers (local agent runs its own flow)', () => {
|
|
32
|
+
const entries = buildExternalMcpEntries([server({ id: 'a', authType: 'oauth' })], { a: { Authorization: 'Bearer should-not-be-used' } });
|
|
33
|
+
expect(entries[0].headers).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
it('emits no auth header for none/unset servers', () => {
|
|
36
|
+
const entries = buildExternalMcpEntries([server({ id: 'a', authType: 'none' }), server({ id: 'b' })], {});
|
|
37
|
+
expect(entries[0].headers).toBeUndefined();
|
|
38
|
+
expect(entries[1].headers).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
it('omits headers when a header-auth server has no resolved credentials', () => {
|
|
41
|
+
const entries = buildExternalMcpEntries([server({ id: 'a', authType: 'header' })], {});
|
|
42
|
+
expect(entries[0].headers).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
it('skips disabled servers', () => {
|
|
45
|
+
const entries = buildExternalMcpEntries([server({ id: 'a', enabled: false })], {});
|
|
46
|
+
expect(entries).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
});
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { execFileSync } from '../utils/subprocess.js';
|
|
3
2
|
import { readFileSync, existsSync } from 'fs';
|
|
4
3
|
import { requireAuth } from '../auth/store.js';
|
|
5
4
|
import { ApiClient } from '../api/client.js';
|
|
6
5
|
import { shouldOutputJson, jsonOut } from '../utils/output.js';
|
|
7
6
|
import { buildDeployGuide, buildErrorResponse } from '../utils/agent-guidance.js';
|
|
7
|
+
import { requireGit } from '../git/preflight.js';
|
|
8
|
+
import { ensureGitIdentity } from '../git/identity.js';
|
|
9
|
+
import { commitWorkingTree } from '../git/auto-commit.js';
|
|
10
|
+
import { syncWithRemote, hasCommits } from '../git/sync.js';
|
|
11
|
+
import { snapshotCriticalFiles, restoreMissingCriticalFiles, commitAndPushRestoredFiles } from '../git/critical-files.js';
|
|
12
|
+
import { evaluateDeployGuard } from '../git/deploy-guard.js';
|
|
13
|
+
import { promptConfirm } from '../utils/prompt.js';
|
|
8
14
|
export const deployCommand = new Command('deploy')
|
|
9
15
|
.description('Deploy the current app to production')
|
|
10
|
-
.
|
|
16
|
+
.option('-y, --yes', 'Skip the confirmation prompt when the working tree differs from the preview')
|
|
17
|
+
.action(async (opts, command) => {
|
|
11
18
|
const useJson = shouldOutputJson(command.optsWithGlobals().json);
|
|
12
19
|
if (!existsSync('.runwork.json')) {
|
|
13
20
|
if (useJson) {
|
|
@@ -17,36 +24,93 @@ export const deployCommand = new Command('deploy')
|
|
|
17
24
|
console.error('No .runwork.json found. Run `runwork init` first.');
|
|
18
25
|
process.exit(1);
|
|
19
26
|
}
|
|
27
|
+
requireGit('deploy');
|
|
20
28
|
const config = JSON.parse(readFileSync('.runwork.json', 'utf-8'));
|
|
21
29
|
const creds = requireAuth();
|
|
22
30
|
const client = new ApiClient(creds);
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
const cwd = process.cwd();
|
|
32
|
+
// Deploy guard: the preview reflects the last *pushed* commit. If the
|
|
33
|
+
// working tree is dirty or ahead of `runwork/main` and no auto-syncing
|
|
34
|
+
// dev session is keeping the preview in lockstep, `runwork deploy` will
|
|
35
|
+
// commit and ship code the user never saw in the preview. Warn first.
|
|
36
|
+
// Runs BEFORE the commit/sync/deploy steps below, which are unchanged.
|
|
37
|
+
const guard = evaluateDeployGuard(cwd, config.appId);
|
|
38
|
+
let deployGuardWarning;
|
|
39
|
+
if (guard.warn) {
|
|
40
|
+
deployGuardWarning = 'You have uncommitted or unpushed changes. `runwork deploy` will commit and deploy them, but they are not reflected in the preview.';
|
|
41
|
+
if (useJson) {
|
|
42
|
+
// Agents must never be blocked: the warning rides along in the
|
|
43
|
+
// final success response below so we proceed without prompting.
|
|
44
|
+
}
|
|
45
|
+
else if (opts.yes || !process.stdin.isTTY) {
|
|
46
|
+
// Non-interactive (no TTY) or explicit --yes: warn and proceed.
|
|
47
|
+
console.warn(deployGuardWarning);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.warn(deployGuardWarning);
|
|
51
|
+
const proceed = await promptConfirm('Deploy these changes anyway?');
|
|
52
|
+
if (!proceed) {
|
|
53
|
+
console.log('Deploy cancelled.');
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Deploy must work even when no `runwork dev` session is (or ever was)
|
|
59
|
+
// running. Reuse the exact sync path dev uses: commit any pending
|
|
60
|
+
// working-tree changes, then fetch -> rebase (fallback merge) -> push.
|
|
61
|
+
// `ensureGitIdentity` seeds a local identity so the commit cannot fail
|
|
62
|
+
// on a fresh machine where git user.email/.name were never configured.
|
|
63
|
+
ensureGitIdentity(cwd, creds);
|
|
64
|
+
if (!useJson)
|
|
65
|
+
console.log('Syncing...');
|
|
25
66
|
try {
|
|
26
|
-
|
|
27
|
-
repoHasCommits = true;
|
|
67
|
+
commitWorkingTree(cwd, `deploy: ${new Date().toISOString().replace('T', ' ').slice(0, 19)}`);
|
|
28
68
|
}
|
|
29
69
|
catch {
|
|
30
|
-
// No
|
|
70
|
+
// No commit created (clean tree) or git not initialized yet. The
|
|
71
|
+
// sync/push step below surfaces the actionable failure.
|
|
72
|
+
}
|
|
73
|
+
if (!hasCommits(cwd)) {
|
|
74
|
+
if (useJson) {
|
|
75
|
+
jsonOut(buildErrorResponse('deploy', 'Nothing to deploy', 'No commits exist and the working tree has no changes to commit, so there is nothing to sync or deploy.', ['Make changes to your app first', 'Run runwork dev to develop and verify changes', 'Then run runwork deploy']));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
console.error('Nothing to deploy. Make changes first, then run `runwork deploy`.');
|
|
79
|
+
process.exit(1);
|
|
31
80
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
81
|
+
// Protect critical files across the sync exactly as `runwork dev` does:
|
|
82
|
+
// syncWithRemote may merge with `-X theirs` (remote wins) to reconcile
|
|
83
|
+
// diverged histories, which can delete `.runwork.json`/`blueprint.json`/
|
|
84
|
+
// `.gitignore` if the remote tip lacks them. Snapshot before, restore after.
|
|
85
|
+
const criticalSnapshot = snapshotCriticalFiles(cwd);
|
|
86
|
+
const syncResult = syncWithRemote(cwd);
|
|
87
|
+
const restoredCriticalFiles = restoreMissingCriticalFiles(cwd, criticalSnapshot);
|
|
88
|
+
if (restoredCriticalFiles.length > 0) {
|
|
89
|
+
const pushed = commitAndPushRestoredFiles(cwd, restoredCriticalFiles);
|
|
90
|
+
if (!useJson) {
|
|
91
|
+
console.warn(`Sync removed critical file(s); restored: ${restoredCriticalFiles.join(', ')}`);
|
|
92
|
+
if (!pushed)
|
|
93
|
+
console.warn('Restoration committed locally but not pushed; will retry on next sync.');
|
|
37
94
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
95
|
+
}
|
|
96
|
+
if (syncResult.status === 'sync-failed') {
|
|
97
|
+
const reason = syncResult.error && syncResult.error !== 'stash-conflict'
|
|
98
|
+
? syncResult.error
|
|
99
|
+
: 'Local history could not be reconciled with the remote.';
|
|
100
|
+
if (useJson) {
|
|
101
|
+
jsonOut(buildErrorResponse('deploy', 'Sync failed before deployment', reason, ['Run git fetch runwork && git rebase runwork/main and resolve any conflicts', 'Or run runwork dev to sync interactively', 'Then retry runwork deploy']));
|
|
102
|
+
process.exit(1);
|
|
41
103
|
}
|
|
42
|
-
|
|
104
|
+
console.error(`Sync failed: ${reason}`);
|
|
105
|
+
console.error('Resolve conflicts (git status), or run `runwork dev`, then retry `runwork deploy`.');
|
|
106
|
+
process.exit(1);
|
|
43
107
|
}
|
|
44
|
-
|
|
108
|
+
if (!syncResult.pushed) {
|
|
45
109
|
if (useJson) {
|
|
46
|
-
jsonOut(buildErrorResponse('deploy', '
|
|
110
|
+
jsonOut(buildErrorResponse('deploy', 'Failed to push before deployment', 'Local commits were synced but could not be pushed to the remote.', ['Check your network connection', 'Run runwork dev to retry syncing', 'Then retry runwork deploy']));
|
|
47
111
|
process.exit(1);
|
|
48
112
|
}
|
|
49
|
-
console.error('
|
|
113
|
+
console.error('Push failed. Check your connection and retry `runwork deploy`.');
|
|
50
114
|
process.exit(1);
|
|
51
115
|
}
|
|
52
116
|
// Trigger deploy
|
|
@@ -65,19 +129,34 @@ export const deployCommand = new Command('deploy')
|
|
|
65
129
|
console.error(`Deploy failed: ${message}`);
|
|
66
130
|
process.exit(1);
|
|
67
131
|
}
|
|
132
|
+
// The server returns a deployment URL only when a deploy actually
|
|
133
|
+
// happened. An empty/missing URL means the deploy was a no-op or did
|
|
134
|
+
// not produce a live deployment, so we must not report success.
|
|
135
|
+
const deploymentUrl = result.deploymentUrl?.trim() ?? '';
|
|
136
|
+
const deployed = deploymentUrl.length > 0;
|
|
137
|
+
if (!deployed) {
|
|
138
|
+
if (useJson) {
|
|
139
|
+
jsonOut(buildErrorResponse('deploy', 'Deployment did not produce a URL', 'The deploy API returned no deployment URL. The deploy may have been a no-op (no changes) or failed server-side.', ['Confirm your changes were committed and pushed (the sync step above succeeded)', 'Run runwork info to check the deployed state', 'Run runwork logs --production to inspect server-side errors', 'Retry runwork deploy']));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
console.error('Deploy did not return a URL. The deploy may have been a no-op or failed server-side.');
|
|
143
|
+
console.error('Run `runwork info` to check state, or `runwork logs --production` to inspect errors.');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
68
146
|
if (useJson) {
|
|
69
147
|
const response = {
|
|
70
148
|
success: true,
|
|
71
149
|
command: 'deploy',
|
|
72
150
|
result: {
|
|
73
|
-
deployed
|
|
74
|
-
url:
|
|
151
|
+
deployed,
|
|
152
|
+
url: deploymentUrl,
|
|
75
153
|
appName: config.appName,
|
|
76
154
|
},
|
|
77
155
|
guide: buildDeployGuide(),
|
|
156
|
+
...(deployGuardWarning ? { warning: deployGuardWarning } : {}),
|
|
78
157
|
};
|
|
79
158
|
jsonOut(response);
|
|
80
159
|
return;
|
|
81
160
|
}
|
|
82
|
-
console.log(`Deployed: ${
|
|
161
|
+
console.log(`Deployed: ${deploymentUrl}`);
|
|
83
162
|
});
|