runwork 0.13.3 → 0.13.4
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-workflow-instance.d.ts +6 -0
- package/bundled-types/workflows.d.ts +1 -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/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 +2 -2
- 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/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
|
@@ -343,5 +343,65 @@ describe('session lifecycle primitives', () => {
|
|
|
343
343
|
});
|
|
344
344
|
expect(file.startedAt).toBe(12345);
|
|
345
345
|
});
|
|
346
|
+
it('defaults noSync to false when omitted', () => {
|
|
347
|
+
const file = buildSessionFile({
|
|
348
|
+
pid: 1,
|
|
349
|
+
sessionId: 's',
|
|
350
|
+
appId: 'a',
|
|
351
|
+
previewUrl: '',
|
|
352
|
+
cliVersion: '0.0.0',
|
|
353
|
+
mode: 'foreground',
|
|
354
|
+
deps: { bootTime: () => 0 },
|
|
355
|
+
});
|
|
356
|
+
expect(file.noSync).toBe(false);
|
|
357
|
+
});
|
|
358
|
+
it('sets noSync when provided', () => {
|
|
359
|
+
const file = buildSessionFile({
|
|
360
|
+
pid: 1,
|
|
361
|
+
sessionId: 's',
|
|
362
|
+
appId: 'a',
|
|
363
|
+
previewUrl: '',
|
|
364
|
+
cliVersion: '0.0.0',
|
|
365
|
+
mode: 'foreground',
|
|
366
|
+
noSync: true,
|
|
367
|
+
deps: { bootTime: () => 0 },
|
|
368
|
+
});
|
|
369
|
+
expect(file.noSync).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
describe('noSync field back-compat', () => {
|
|
373
|
+
it('round-trips noSync through write/read', () => {
|
|
374
|
+
const file = buildSessionFile({
|
|
375
|
+
pid: process.pid,
|
|
376
|
+
sessionId: 's',
|
|
377
|
+
appId: 'a',
|
|
378
|
+
previewUrl: 'https://x',
|
|
379
|
+
cliVersion: '0.0.0',
|
|
380
|
+
mode: 'detached',
|
|
381
|
+
noSync: true,
|
|
382
|
+
deps: { bootTime: () => 0 },
|
|
383
|
+
});
|
|
384
|
+
writeSessionFile(appDir, file);
|
|
385
|
+
expect(readSessionFile(appDir)?.noSync).toBe(true);
|
|
386
|
+
});
|
|
387
|
+
it('validates a v1-style file that lacks noSync and reads it back falsy', () => {
|
|
388
|
+
const { dir, file } = getSessionPaths(appDir);
|
|
389
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
390
|
+
// v1 file shape: every required field present, no noSync key at all.
|
|
391
|
+
const v1 = makeValidSessionFile();
|
|
392
|
+
// makeValidSessionFile does not add noSync, so this is already v1-shaped.
|
|
393
|
+
expect('noSync' in v1).toBe(false);
|
|
394
|
+
fs.writeFileSync(file, JSON.stringify(v1), 'utf-8');
|
|
395
|
+
const read = readSessionFile(appDir);
|
|
396
|
+
expect(read).not.toBeNull();
|
|
397
|
+
expect(read?.noSync).toBeFalsy();
|
|
398
|
+
});
|
|
399
|
+
it('rejects a file whose noSync field is the wrong type', () => {
|
|
400
|
+
const { dir, file } = getSessionPaths(appDir);
|
|
401
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
402
|
+
const bad = { ...makeValidSessionFile(), noSync: 'yes' };
|
|
403
|
+
fs.writeFileSync(file, JSON.stringify(bad), 'utf-8');
|
|
404
|
+
expect(readSessionFile(appDir)).toBeNull();
|
|
405
|
+
});
|
|
346
406
|
});
|
|
347
407
|
});
|
package/dist/dev/session.d.ts
CHANGED
|
@@ -33,6 +33,13 @@ export interface SessionFile {
|
|
|
33
33
|
bootTime: number;
|
|
34
34
|
cliVersion: string;
|
|
35
35
|
mode: SessionMode;
|
|
36
|
+
/**
|
|
37
|
+
* Whether this session runs in no-sync mode (no file watcher, no
|
|
38
|
+
* auto-commit, no working-tree-mutating startup). Optional for
|
|
39
|
+
* back-compat with v1 session files that predate the field; absent is
|
|
40
|
+
* treated as `false`.
|
|
41
|
+
*/
|
|
42
|
+
noSync?: boolean;
|
|
36
43
|
}
|
|
37
44
|
export type SessionStaleReason = 'malformed' | 'version-mismatch' | 'app-id-mismatch' | 'boot-time-mismatch' | 'pid-dead';
|
|
38
45
|
export type SessionState = {
|
|
@@ -153,6 +160,7 @@ export declare function buildSessionFile(input: {
|
|
|
153
160
|
previewUrl: string;
|
|
154
161
|
cliVersion: string;
|
|
155
162
|
mode: SessionMode;
|
|
163
|
+
noSync?: boolean;
|
|
156
164
|
startedAt?: number;
|
|
157
165
|
deps?: SessionDeps;
|
|
158
166
|
}): SessionFile;
|
package/dist/dev/session.js
CHANGED
|
@@ -111,7 +111,9 @@ function validateSessionFile(raw) {
|
|
|
111
111
|
typeof r.startedAt === 'number' &&
|
|
112
112
|
typeof r.bootTime === 'number' &&
|
|
113
113
|
typeof r.cliVersion === 'string' &&
|
|
114
|
-
(r.mode === 'foreground' || r.mode === 'detached')
|
|
114
|
+
(r.mode === 'foreground' || r.mode === 'detached') &&
|
|
115
|
+
// Optional for back-compat: v1 files lack the field. Accept absent or boolean.
|
|
116
|
+
(r.noSync === undefined || typeof r.noSync === 'boolean'));
|
|
115
117
|
}
|
|
116
118
|
/**
|
|
117
119
|
* Read and parse the session file. Returns `null` for a missing file or
|
|
@@ -248,5 +250,6 @@ export function buildSessionFile(input) {
|
|
|
248
250
|
bootTime: d.bootTime(),
|
|
249
251
|
cliVersion: input.cliVersion,
|
|
250
252
|
mode: input.mode,
|
|
253
|
+
noSync: input.noSync ?? false,
|
|
251
254
|
};
|
|
252
255
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Auto-generated by scripts/embed-types.ts -- do not edit
|
|
2
2
|
export const BUNDLED_TYPES = {
|
|
3
|
-
"core-workflow-instance.d.ts": "/**\n * WorkflowInstance Durable Object - Native Workflow Engine\n * DO NOT MODIFY THIS FILE - You may break the project functionality\n *\n * One instance per workflow execution. Uses alarm-based execution\n * to bypass the 30-second CPU limit and provide durability.\n */\nimport { DurableObject } from 'cloudflare:workers';\nimport type { NativeWorkflowInstance, NativeWorkflowConfig } from './core-workflow-types';\nimport type { StepOptions, WaitEventOptions } from './core-workflows';\nimport { type Env } from './core-utils';\n/**\n * WorkflowInstance Durable Object\n *\n * Manages the execution of a single workflow instance.\n * Uses alarm chaining for step execution to bypass CPU limits.\n */\nexport declare class WorkflowInstanceDO extends DurableObject<Env> {\n private instance;\n private definition;\n private stepCounter;\n /**\n * Create and start a new workflow instance\n */\n create(workflowName: string, input: unknown, instanceId: string, config?: Partial<NativeWorkflowConfig>, metadata?: Record<string, unknown>): Promise<void>;\n /**\n * Get current workflow status\n */\n getStatus(): Promise<NativeWorkflowInstance | null>;\n /**\n * Signal the workflow with an event (for waitForEvent steps)\n */\n signal(eventType: string, payload: unknown): Promise<void>;\n /**\n * Pause the workflow\n */\n pause(): Promise<void>;\n /**\n * Resume a paused workflow\n */\n resume(): Promise<void>;\n /**\n * Cancel the workflow\n */\n cancel(): Promise<void>;\n /**\n * Alarm handler - main execution loop\n */\n alarm(): Promise<void>;\n /**\n * Execute the workflow handler\n */\n private executeWorkflow;\n /**\n * Execute or retrieve cached result for a step\n */\n executeStep<T>(name: string, options: StepOptions | undefined, fn: () => Promise<T>): Promise<T>;\n /**\n * Sleep for a duration\n */\n executeSleep(name: string, duration: string): Promise<void>;\n /**\n * Sleep until a specific timestamp\n */\n executeSleepUntil(name: string, timestamp: Date): Promise<void>;\n /**\n * Wait for an external event\n */\n executeWaitForEvent<T>(name: string, options?: WaitEventOptions): Promise<T>;\n private ensureLoaded;\n private persist;\n private isTerminal;\n private isTimedOut;\n private getCurrentStep;\n private transitionTo;\n private notifyCoordinator;\n private generateStepId;\n private parseDuration;\n private calculateBackoff;\n private executeWithTimeout;\n private normalizeError;\n}\n",
|
|
3
|
+
"core-workflow-instance.d.ts": "/**\n * WorkflowInstance Durable Object - Native Workflow Engine\n * DO NOT MODIFY THIS FILE - You may break the project functionality\n *\n * One instance per workflow execution. Uses alarm-based execution\n * to bypass the 30-second CPU limit and provide durability.\n */\nimport { DurableObject } from 'cloudflare:workers';\nimport type { NativeWorkflowInstance, NativeWorkflowConfig } from './core-workflow-types';\nimport type { StepOptions, WaitEventOptions } from './core-workflows';\nimport { type Env } from './core-utils';\nexport declare class WorkflowPausedError extends Error {\n reason: 'sleep' | 'waitEvent';\n resumeAt?: number | undefined;\n constructor(reason: 'sleep' | 'waitEvent', resumeAt?: number | undefined);\n}\nexport declare function isWorkflowControlSignal(error: unknown): boolean;\n/**\n * WorkflowInstance Durable Object\n *\n * Manages the execution of a single workflow instance.\n * Uses alarm chaining for step execution to bypass CPU limits.\n */\nexport declare class WorkflowInstanceDO extends DurableObject<Env> {\n private instance;\n private definition;\n private stepCounter;\n /**\n * Create and start a new workflow instance\n */\n create(workflowName: string, input: unknown, instanceId: string, config?: Partial<NativeWorkflowConfig>, metadata?: Record<string, unknown>): Promise<void>;\n /**\n * Get current workflow status\n */\n getStatus(): Promise<NativeWorkflowInstance | null>;\n /**\n * Signal the workflow with an event (for waitForEvent steps)\n */\n signal(eventType: string, payload: unknown): Promise<void>;\n /**\n * Pause the workflow\n */\n pause(): Promise<void>;\n /**\n * Resume a paused workflow\n */\n resume(): Promise<void>;\n /**\n * Cancel the workflow\n */\n cancel(): Promise<void>;\n /**\n * Alarm handler - main execution loop\n */\n alarm(): Promise<void>;\n /**\n * Execute the workflow handler\n */\n private executeWorkflow;\n /**\n * Execute or retrieve cached result for a step\n */\n executeStep<T>(name: string, options: StepOptions | undefined, fn: () => Promise<T>): Promise<T>;\n /**\n * Sleep for a duration\n */\n executeSleep(name: string, duration: string): Promise<void>;\n /**\n * Sleep until a specific timestamp\n */\n executeSleepUntil(name: string, timestamp: Date): Promise<void>;\n /**\n * Wait for an external event\n */\n executeWaitForEvent<T>(name: string, options?: WaitEventOptions): Promise<T>;\n private ensureLoaded;\n private persist;\n private isTerminal;\n private isTimedOut;\n private getCurrentStep;\n private transitionTo;\n private notifyCoordinator;\n private generateStepId;\n private parseDuration;\n private calculateBackoff;\n private executeWithTimeout;\n private normalizeError;\n}\n",
|
|
4
4
|
"core-agent.d.ts": "/**\n * Core Agent Types and Utilities\n * DO NOT MODIFY THIS FILE - You may break the agent functionality\n *\n * This module provides types and utilities for AI agents.\n * The actual agent implementation is in core-base-agent.ts.\n *\n * @example\n * ```typescript\n * // In agents.ts\n * import type { AgentDefinition } from './core-agent';\n *\n * const supportAgent: AgentDefinition = {\n * name: 'support-assistant',\n * description: 'Helps users with common questions',\n * systemPrompt: 'You are a helpful support assistant...',\n * integrations: ['slack', 'github'],\n * entities: ['ticket', 'user'],\n * };\n *\n * export const APP_AGENTS: AgentDefinition[] = [supportAgent];\n * ```\n */\nimport { z } from 'zod';\nimport type { Env } from './core-utils';\nimport type { IntegrationClient } from './core-integrations';\nimport type { ExecutionLimits } from './task-delegation/types';\n/**\n * Agent state persisted across conversations\n */\nexport interface AgentState {\n /** Conversation history */\n messages: AgentMessage[];\n /** Custom memory data */\n memory: Record<string, unknown>;\n /** Session metadata */\n sessionId: string;\n /** User identifier for per-user memory */\n userId?: string;\n /** Last activity timestamp */\n lastActivityAt: number;\n}\n/**\n * Message in agent conversation\n */\nexport interface AgentMessage {\n id: string;\n role: 'user' | 'assistant' | 'system' | 'tool';\n content: string;\n timestamp: number;\n toolCalls?: Array<{\n id: string;\n name: string;\n arguments: Record<string, unknown>;\n }>;\n toolResults?: Array<{\n id: string;\n result: unknown;\n }>;\n}\n/**\n * Memory strategy for agents\n * - session: Memory resets when conversation ends\n * - user: Memory persists per user across conversations\n * - shared: Memory is shared across all users\n */\nexport type MemoryStrategy = 'session' | 'user' | 'shared';\n/**\n * Agent definition schema\n */\nexport interface AgentDefinition {\n /** Unique agent name (kebab-case, e.g., 'support-assistant') */\n name: string;\n /** Human-readable description */\n description: string;\n /** System prompt defining agent behavior */\n systemPrompt: string;\n /** Agent type for UI hints */\n type?: 'conversational' | 'task';\n /** Integration IDs the agent can access */\n integrations?: string[];\n /** Entity names the agent can read/write */\n entities?: string[];\n /** Memory strategy */\n memoryStrategy?: MemoryStrategy;\n /** Custom tools for this agent */\n customTools?: AgentToolDefinition[];\n /** Default task instruction for task agents. Can be overridden per execution. */\n prompt?: string;\n /** Cost/time limits for task agent sandbox execution. Optional overrides. */\n executionLimits?: ExecutionLimits;\n}\n/**\n * Custom tool definition for agents\n */\nexport interface AgentToolDefinition {\n name: string;\n description: string;\n parameters: z.ZodObject<z.ZodRawShape>;\n execute: (args: Record<string, unknown>, context: AgentToolContext) => Promise<unknown>;\n}\n/**\n * Context passed to tool execution\n */\nexport interface AgentToolContext {\n env: Env;\n agentName: string;\n integrationClient: IntegrationClient;\n state: AgentState;\n}\n/**\n * Initial state for new agent instances\n */\nexport declare const INITIAL_AGENT_STATE: AgentState;\n/**\n * Agent registry - maps agent names to their definitions\n */\nexport type AgentRegistry = Record<string, AgentDefinition>;\n/**\n * Create an agent registry from an array of definitions.\n *\n * NOTE: This function is defensive against undefined/null inputs because it's called\n * at module-level in core-base-agent.ts. During Vite HMR, when files are being rewritten,\n * there's a race condition where APP_AGENTS may be undefined momentarily. Without\n * this guard, the worker crashes with \"definitions is not iterable\", which triggers\n * cascading HMR failures and eventually causes \"@cloudflare/vite-plugin\" to lose its\n * miniflare instance (\"Expected `miniflare` to be defined\" error).\n */\nexport declare function createAgentRegistry(definitions?: AgentDefinition[] | null): AgentRegistry;\n/**\n * Workspace agent registration request\n */\nexport interface RegisterAgentRequest {\n appId: string;\n appName: string;\n agentName: string;\n description: string;\n type: 'conversational' | 'task';\n integrations: string[];\n entities: string[];\n tools?: AgentToolMeta[];\n systemPrompt?: string;\n prompt?: string;\n executionLimits?: ExecutionLimits;\n}\n/**\n * Tool metadata for API responses (JSON-serializable, no Zod)\n */\nexport interface AgentToolMeta {\n name: string;\n description: string;\n source: 'entity' | 'integration' | 'custom';\n sourceId?: string;\n parameters: {\n type: 'object';\n properties: Record<string, {\n type: string;\n description?: string;\n }>;\n required?: string[];\n };\n}\n/**\n * Tool definition with Zod schema (for buildTools)\n */\nexport interface AgentToolDef {\n name: string;\n description: string;\n source: 'entity' | 'integration' | 'custom';\n sourceId?: string;\n parameters: z.ZodObject<z.ZodRawShape>;\n}\n/** Zod schema for entity list tool */\nexport declare const entityListSchema: z.ZodObject<{\n limit: z.ZodOptional<z.ZodNumber>;\n cursor: z.ZodOptional<z.ZodString>;\n}, \"strip\", z.ZodTypeAny, {\n cursor?: string | undefined;\n limit?: number | undefined;\n}, {\n cursor?: string | undefined;\n limit?: number | undefined;\n}>;\n/** Zod schema for entity get tool */\nexport declare const entityGetSchema: z.ZodObject<{\n id: z.ZodString;\n}, \"strip\", z.ZodTypeAny, {\n id: string;\n}, {\n id: string;\n}>;\n/**\n * Get tool definitions for entity access\n */\nexport declare function getEntityToolDefs(entities: string[]): AgentToolDef[];\n/** Zod schema for integration API tool */\nexport declare const integrationApiSchema: z.ZodObject<{\n method: z.ZodDefault<z.ZodEnum<[\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"]>>;\n endpoint: z.ZodString;\n data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;\n}, \"strip\", z.ZodTypeAny, {\n method: \"POST\" | \"GET\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n endpoint: string;\n data?: Record<string, unknown> | undefined;\n}, {\n endpoint: string;\n data?: Record<string, unknown> | undefined;\n method?: \"POST\" | \"GET\" | \"PUT\" | \"PATCH\" | \"DELETE\" | undefined;\n}>;\n/**\n * Get tool definitions for integration access\n */\nexport declare function getIntegrationToolDefs(integrations: string[]): AgentToolDef[];\n/**\n * Get tool definitions from custom tools (extracts metadata from AgentToolDefinition)\n */\nexport declare function getCustomToolDefs(customTools: AgentToolDefinition[]): AgentToolDef[];\n/**\n * Get all tool definitions for an agent\n */\nexport declare function getAgentToolDefs(agent: AgentDefinition): AgentToolDef[];\n/**\n * Convert tool definition to JSON-serializable metadata\n */\nexport declare function toolDefToMeta(def: AgentToolDef): AgentToolMeta;\n/**\n * Get all tool metadata for an agent (JSON-serializable)\n * Use this for API responses and workspace registration\n */\nexport declare function getAgentToolMeta(agent: AgentDefinition): AgentToolMeta[];\n/**\n * Tool availability status at runtime\n */\nexport interface AgentToolStatus {\n name: string;\n source: 'entity' | 'integration' | 'custom';\n sourceId?: string;\n available: boolean;\n reason?: string;\n}\n/**\n * Integration connection status\n */\nexport interface IntegrationStatus {\n id: string;\n connected: boolean;\n reason?: string;\n}\n/**\n * Agent runtime status - actual availability of capabilities\n */\nexport interface AgentRuntimeStatus {\n /** Agent name */\n name: string;\n /** Whether agent definition was found */\n initialized: boolean;\n /** Integration connection status */\n integrations: IntegrationStatus[];\n /** Tool availability status */\n tools: AgentToolStatus[];\n /** AI proxy configured */\n aiProxyConfigured: boolean;\n /** Timestamp of status check */\n checkedAt: number;\n}\nimport type { Hono } from 'hono';\n/**\n * Mount agent routes on the Hono app\n * Provides REST API for AI agent interactions\n */\nexport declare function agentRoutes(app: Hono<{\n Bindings: Env;\n}>): void;\n",
|
|
5
5
|
"core-base-agent.d.ts": "/**\n * Base Agent - Durable Object for AI Agents\n * DO NOT MODIFY THIS FILE - You may break the agent functionality\n *\n * This is the Durable Object class for AI agents built on Cloudflare's Agents SDK.\n * It extends AIChatAgent which provides:\n * - WebSocket connection handling\n * - Message persistence (SQLite)\n * - Streaming response resumption\n * - State management\n *\n * LLM calls go through runwork's AI proxy, which:\n * - Handles API keys securely (no keys in vibe-app)\n * - Provides analytics, caching, and rate limiting via AI Gateway\n * - Supports multiple providers (OpenAI, Anthropic, etc.)\n *\n * Agent definitions are loaded from the APP_AGENTS registry based on\n * the agent name stored in the DO's storage.\n */\nimport { AIChatAgent } from 'agents/ai-chat-agent';\nimport { type StreamTextOnFinishCallback, type ToolSet } from 'ai';\nimport { type Env } from './core-utils';\nimport { type AgentDefinition, type AgentRuntimeStatus } from './core-agent';\n/**\n * BaseAgent - The Durable Object for all agents\n *\n * This class extends AIChatAgent directly, which means:\n * - The class itself IS the Durable Object (not wrapped in another DO)\n * - AIChatAgent handles WebSocket connections, message persistence, and streaming\n * - We implement onChatMessage() to process incoming messages using the AI SDK\n *\n * Agent instances are identified by DO ID format: \"{agentName}:{sessionId}\"\n */\nexport declare class BaseAgent extends AIChatAgent<Env> {\n private definition;\n private integrationClient;\n private agentName;\n /**\n * Called when the agent starts (on first request or after hibernation)\n * Load the agent definition from storage\n */\n onStart(): Promise<void>;\n /**\n * Set the agent name and load its definition\n * Called when initializing a new agent instance\n */\n setAgentName(name: string): Promise<void>;\n /**\n * Handle incoming chat messages using AI SDK\n * This is the main entry point called by AIChatAgent when a message arrives\n */\n onChatMessage(onFinish: StreamTextOnFinishCallback<ToolSet>): Promise<Response>;\n /**\n * Build tools from agent definition using AI SDK tool() helper\n */\n private buildTools;\n /**\n * Build tools for accessing integrations using AI SDK tool() helper\n * Uses shared definitions from core-agent.ts, wraps with executors\n * @param integrations - List of integration IDs to build tools for\n */\n private buildIntegrationTools;\n /**\n * Build tools for accessing entities using AI SDK tool() helper\n * Uses entity classes directly with EntityContext\n */\n private buildEntityTools;\n /**\n * Build tools for running complex tasks via a sandboxed AI sub-agent\n */\n private buildCodeExecutionTools;\n /**\n * Get integration client (lazy-loaded)\n */\n private getIntegrationClient;\n /**\n * Get tool execution context\n */\n private getToolContext;\n /**\n * Get the agent's name\n */\n getAgentName(): string | null;\n /**\n * Get the agent's definition\n */\n getDefinition(): AgentDefinition | null;\n /**\n * Get runtime status - actual availability of tools and integrations\n * This checks real connectivity, not just declared capabilities\n */\n getStatus(): Promise<AgentRuntimeStatus>;\n}\n",
|
|
6
6
|
"core-events.d.ts": "/**\n * Core Events\n * DO NOT MODIFY THIS FILE - You may break the project functionality\n *\n * This module provides:\n * - emitEvent(): Fire-and-forget event emission to the workspace event stream\n * - flog(): Structured logging that outputs JSON to sandbox stdout\n *\n * Events appear in the workspace activity feed and channel views.\n * When running outside a workspace (standalone mode), calls are silently skipped.\n */\nexport interface EmitEventParams {\n type: string;\n entityType?: string;\n entityId?: string;\n summary: string;\n metadata?: Record<string, unknown>;\n}\ninterface EventEnv {\n WORKSPACE_API_URL?: string;\n WORKSPACE_API_KEY?: string;\n WORKSPACE_ID?: string;\n APP_ID?: string;\n APP_NAME?: string;\n DEPLOYMENT_MODE?: 'preview' | 'production';\n WorkspaceObject?: DurableObjectNamespace;\n}\n/**\n * Any context that supports waitUntil - compatible with both\n * ExecutionContext (Hono route handlers) and DurableObjectState (DOs).\n */\ntype WaitUntilContext = Pick<ExecutionContext, 'waitUntil'>;\n/**\n * Convert an app name to a kebab-case channel name.\n * \"My Cool App\" -> \"my-cool-app\"\n */\nexport declare function toChannelName(appName: string): string;\n/**\n * Structured log entry for sandbox stdout.\n * Outputs JSON with a consistent schema so the platform can parse and display\n * these entries in the observability timeline.\n */\nexport declare function flog(level: 'info' | 'warn' | 'error', system: string, message: string, data?: Record<string, unknown>): void;\n/**\n * Emit an event to the workspace unified event stream.\n * Fire-and-forget: uses ctx.waitUntil so it doesn't block the response.\n * Silently skips if workspace env vars are not configured (standalone mode).\n *\n * Automatically routes events to a channel derived from APP_NAME when available.\n */\nexport declare function emitEvent(ctx: WaitUntilContext, env: EventEnv, event: EmitEventParams): void;\nexport {};\n",
|
|
@@ -36,7 +36,7 @@ export const BUNDLED_TYPES = {
|
|
|
36
36
|
"core-integration-entities.d.ts": "/**\n * Core Integration Entity Utilities\n * DO NOT MODIFY THIS FILE - You may break the integration entity functionality\n *\n * This module provides the IntegrationEntity base class for entities that are backed\n * by external integrations (HubSpot, GitHub, Salesforce, etc.) instead of local storage.\n *\n * IntegrationEntity implements the unified EntityClass interface with EntityContext.\n * All CRUD operations take ctx as the first parameter, matching the Entity class pattern.\n */\nimport type { EntityContext, ListOptions, PaginatedResult, EntityClass } from './core-entities';\nimport type { Env } from './core-utils';\nexport interface OperationConfig {\n /** API endpoint path (use :id for ID placeholder) */\n endpoint: string;\n}\nexport interface IntegrationEntityConfig {\n /** Integration identifier: \"hubspot\", \"github\", \"salesforce\", etc. */\n integrationId: string;\n /** Nango provider config key: \"hubspot\", \"github\", etc. */\n providerConfigKey: string;\n operations: {\n list?: OperationConfig;\n get?: OperationConfig;\n create?: OperationConfig;\n update?: OperationConfig;\n delete?: OperationConfig;\n };\n}\n/**\n * Extract the entity data type from an IntegrationEntity constructor.\n * e.g., IntegrationEntityData<typeof ContactEntity> = HubSpotContact\n */\ntype IntegrationEntityData<T> = T extends abstract new (...args: unknown[]) => IntegrationEntity<infer D> ? D : never;\n/**\n * Base constructor type for IntegrationEntity subclasses\n */\ntype IntegrationEntityCtor = abstract new (...args: unknown[]) => IntegrationEntity<{\n id: string;\n}>;\n/**\n * Constructor with required static properties.\n * This ensures the calling class has entityName, config, etc.\n */\ntype IntegrationEntityCtorWithConfig<TCtor> = TCtor & {\n entityName: string;\n config: IntegrationEntityConfig;\n schema?: Record<string, unknown>;\n getConnectionId(env: Env | Record<string, unknown>): string | undefined;\n};\n/**\n * IntegrationEntityClass type - for type checking integration entity classes\n */\nexport interface IntegrationEntityClass<T extends {\n id: string;\n} = {\n id: string;\n}> extends EntityClass<T> {\n config: IntegrationEntityConfig;\n}\n/**\n * IntegrationEntity - Base class for entities backed by external integrations\n *\n * Implements the unified EntityClass interface with EntityContext as first parameter.\n * This makes IntegrationEntity and Entity have identical CRUD interfaces.\n *\n * Extend this class to define entities that proxy CRUD operations to external services\n * like HubSpot, GitHub, Salesforce, etc. via the IntegrationClient.\n *\n * @example\n * ```typescript\n * import { IntegrationEntity, IntegrationEntityConfig } from './core-integration-entities';\n *\n * interface HubSpotContact {\n * id: string;\n * properties: { email: string; firstname: string; lastname: string };\n * }\n *\n * export class ContactEntity extends IntegrationEntity<HubSpotContact> {\n * static readonly entityName = 'hubspot_contact';\n * static readonly config: IntegrationEntityConfig = {\n * integrationId: 'hubspot',\n * providerConfigKey: 'hubspot',\n * operations: {\n * list: { endpoint: '/crm/v3/objects/contacts' },\n * get: { endpoint: '/crm/v3/objects/contacts/:id' },\n * create: { endpoint: '/crm/v3/objects/contacts' },\n * update: { endpoint: '/crm/v3/objects/contacts/:id' },\n * delete: { endpoint: '/crm/v3/objects/contacts/:id' },\n * },\n * };\n * static readonly schema = { fields: ['id', 'email', 'firstname', 'lastname'] };\n * }\n *\n * // Usage (same pattern as Entity):\n * const ctx = { env, client };\n * const contacts = await ContactEntity.list(ctx, { limit: 50 });\n * // contacts is PaginatedResult<HubSpotContact> - properly typed!\n * const contact = await ContactEntity.get(ctx, 'contact-123');\n * // contact is HubSpotContact | null - properly typed!\n * ```\n */\nexport declare abstract class IntegrationEntity<T extends {\n id: string;\n}> {\n static readonly entityName: string;\n static readonly config: IntegrationEntityConfig;\n static readonly schema?: Record<string, unknown>;\n /**\n * Get connection ID from env var if explicitly set.\n * Returns undefined when not set -- the proxy resolves connections\n * dynamically via workspaceId + provider, so this is optional.\n */\n static getConnectionId(env: Env | Record<string, unknown>): string | undefined;\n /**\n * Get the IntegrationClient from context\n * Throws if client is not available\n */\n private static getClient;\n /**\n * List entities from the integration with optional filters\n * Filters are passed through as query parameters to the external API\n * Note: Entities needing POST-based filtering should override this method\n */\n static list<TCtor extends IntegrationEntityCtor>(this: IntegrationEntityCtorWithConfig<TCtor>, ctx: EntityContext, options?: ListOptions): Promise<PaginatedResult<IntegrationEntityData<TCtor>>>;\n /**\n * Get a single entity by ID from the integration\n */\n static get<TCtor extends IntegrationEntityCtor>(this: IntegrationEntityCtorWithConfig<TCtor>, ctx: EntityContext, id: string): Promise<IntegrationEntityData<TCtor> | null>;\n /**\n * Create a new entity in the integration\n */\n static create<TCtor extends IntegrationEntityCtor>(this: IntegrationEntityCtorWithConfig<TCtor>, ctx: EntityContext, data: Partial<IntegrationEntityData<TCtor>>): Promise<IntegrationEntityData<TCtor>>;\n /**\n * Update an entity in the integration\n */\n static update<TCtor extends IntegrationEntityCtor>(this: IntegrationEntityCtorWithConfig<TCtor>, ctx: EntityContext, id: string, data: Partial<IntegrationEntityData<TCtor>>): Promise<IntegrationEntityData<TCtor>>;\n /**\n * Delete an entity from the integration\n */\n static delete<TCtor extends IntegrationEntityCtor>(this: IntegrationEntityCtorWithConfig<TCtor>, ctx: EntityContext, id: string): Promise<void>;\n}\n/**\n * Type guard to check if a class is an IntegrationEntity\n */\nexport declare function isIntegrationEntity(cls: unknown): cls is IntegrationEntityClass;\nexport {};\n",
|
|
37
37
|
"core-types.d.ts": "/**\n * This file defines default types provided by the platform.\n * DO NOT EDIT THIS FILE. You can define your own types in `shared/types.ts` file.\n */\nexport interface ApiResponse<T = unknown> {\n success: boolean;\n data?: T;\n error?: string;\n}\n/**\n * Response from a Nango action or sync trigger\n */\nexport interface NangoActionResponse<T = unknown> {\n success: boolean;\n data?: T;\n error?: string;\n /** True if error is due to missing integration connection (user hasn't connected yet) */\n isConnectionError?: boolean;\n /** True if error is due to insufficient permissions/scopes (401/403 from external API) */\n isPermissionError?: boolean;\n /** HTTP status code from the external API (if available) */\n statusCode?: number;\n}\n/**\n * Nango proxy request options - used for direct API calls through Nango\n */\nexport interface NangoProxyOptions {\n method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n endpoint: string;\n providerConfigKey: string;\n data?: Record<string, unknown>;\n headers?: Record<string, string>;\n params?: Record<string, string>;\n retries?: number;\n}\n/**\n * Metadata for a stored file\n */\nexport interface FileMetadata {\n id: string;\n key: string;\n name: string;\n size: number;\n contentType: string;\n uploadedAt: number;\n uploadedBy?: string;\n customMetadata?: Record<string, string>;\n}\n/**\n * Options for uploading a file\n */\nexport interface UploadOptions {\n key?: string;\n name?: string;\n contentType?: string;\n uploadedBy?: string;\n customMetadata?: Record<string, string>;\n}\n/**\n * Options for downloading a file\n */\nexport interface DownloadOptions {\n range?: {\n offset?: number;\n length?: number;\n };\n}\n/**\n * Options for listing files\n */\nexport interface ListFilesOptions {\n prefix?: string;\n limit?: number;\n cursor?: string;\n delimiter?: string;\n}\n/**\n * Result of listing files\n */\nexport interface ListFilesResult {\n files: FileMetadata[];\n cursor?: string;\n truncated: boolean;\n}\n/**\n * Request for generating a presigned URL\n */\nexport interface PresignedUrlRequest {\n appId: string;\n key: string;\n action: 'read' | 'write';\n contentType?: string;\n expiresIn?: number;\n}\n/**\n * Response containing a presigned URL\n */\nexport interface PresignedUrlResponse {\n url: string;\n expiresAt: number;\n}\n",
|
|
38
38
|
"core-workflow-types.d.ts": "/**\n * Workflow Types - Shared types for native workflow engine\n * DO NOT MODIFY THIS FILE - You may break the project functionality\n */\nimport type { Env } from './core-utils';\nimport type { IntegrationClient } from './core-integrations';\nexport type NativeWorkflowStatus = 'pending' | 'running' | 'sleeping' | 'waiting' | 'paused' | 'completed' | 'failed' | 'cancelled' | 'timedOut';\nexport type NativeStepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';\nexport type NativeStepType = 'do' | 'sleep' | 'sleepUntil' | 'waitEvent';\nexport interface NativeWorkflowError {\n code: string;\n message: string;\n stack?: string;\n retryable: boolean;\n}\nexport interface NativeStepAttempt {\n attemptNumber: number;\n startedAt: number;\n completedAt?: number;\n error?: NativeWorkflowError;\n}\nexport interface NativeStepExecution {\n id: string;\n name: string;\n type: NativeStepType;\n status: NativeStepStatus;\n input?: unknown;\n output?: unknown;\n error?: NativeWorkflowError;\n attempts: NativeStepAttempt[];\n maxRetries: number;\n startedAt?: number;\n completedAt?: number;\n timeoutMs?: number;\n sleepUntil?: number;\n eventType?: string;\n eventTimeoutAt?: number;\n}\nexport interface NativeWorkflowInstance {\n id: string;\n workflowName: string;\n status: NativeWorkflowStatus;\n input: unknown;\n output?: unknown;\n error?: NativeWorkflowError;\n createdAt: number;\n startedAt?: number;\n completedAt?: number;\n steps: NativeStepExecution[];\n currentStepId?: string;\n config: NativeWorkflowConfig;\n metadata?: Record<string, unknown>;\n}\nexport interface NativeWorkflowConfig {\n maxRetries: number;\n retryBackoffMs: number;\n stepTimeoutMs: number;\n workflowTimeoutMs: number;\n}\nexport declare const DEFAULT_WORKFLOW_CONFIG: NativeWorkflowConfig;\nexport interface NativeStepContext {\n /** Unique instance ID for this workflow execution */\n instanceId: string;\n /** Input passed when workflow was started */\n workflowInput: unknown;\n /** Results from previous steps */\n previousSteps: Record<string, {\n output: unknown;\n status: NativeStepStatus;\n }>;\n}\nexport interface WorkflowIndexEntry {\n instanceId: string;\n workflowName: string;\n status: NativeWorkflowStatus;\n createdAt: number;\n completedAt?: number;\n}\nexport interface ListWorkflowsOptions {\n workflowName?: string;\n status?: NativeWorkflowStatus;\n limit?: number;\n offset?: number;\n}\nexport interface ExecuteStepRequest {\n instanceId: string;\n stepId: string;\n stepName: string;\n workflowName: string;\n workflowInput: unknown;\n previousSteps: Record<string, {\n output: unknown;\n status: NativeStepStatus;\n }>;\n}\nexport interface ExecuteStepResponse {\n success: boolean;\n output?: unknown;\n error?: NativeWorkflowError;\n}\nexport interface PendingEvent {\n instanceId: string;\n stepId: string;\n eventType: string;\n waitingSince: number;\n timeoutAt?: number;\n}\nexport interface ReceivedEvent {\n type: string;\n payload: unknown;\n timestamp: number;\n}\n/**\n * Result returned by a workflow execution\n */\nexport interface WorkflowResult {\n success: boolean;\n [key: string]: unknown;\n}\n/**\n * Possible states of a workflow instance\n */\nexport type WorkflowStatus = 'queued' | 'running' | 'paused' | 'errored' | 'terminated' | 'complete' | 'waiting';\n/**\n * Information about a workflow instance\n */\nexport interface WorkflowInstanceInfo {\n id: string;\n workflowName: string;\n status: WorkflowStatus;\n params?: Record<string, unknown>;\n createdAt: number;\n startedAt?: number;\n completedAt?: number;\n error?: string;\n result?: WorkflowResult;\n}\n/**\n * Interface for WorkflowInstance DO stub methods\n * Used to type the DurableObjectStub without importing the class\n */\nexport interface WorkflowInstanceStub {\n create(workflowName: string, params: Record<string, unknown>, instanceId: string, config?: Partial<NativeWorkflowConfig>, metadata?: Record<string, unknown>): Promise<void>;\n getStatus(): Promise<NativeWorkflowInstance | null>;\n signal(eventType: string, payload: unknown): Promise<void>;\n pause(): Promise<void>;\n resume(): Promise<void>;\n cancel(): Promise<void>;\n}\n/**\n * Interface for WorkflowCoordinator DO stub methods\n * Used to type the DurableObjectStub without importing the class\n */\nexport interface WorkflowCoordinatorStub {\n create(workflowName: string, params: Record<string, unknown>, config?: Partial<NativeWorkflowConfig>, metadata?: Record<string, unknown>): Promise<string>;\n signal(instanceId: string, eventType: string, payload: unknown): Promise<void>;\n getInstanceInfo(instanceId: string): Promise<WorkflowInstanceInfo | null>;\n listInstances(options?: ListWorkflowsOptions): Promise<WorkflowInstanceInfo[]>;\n updateIndex(instanceId: string, workflowName: string, status: NativeWorkflowStatus): Promise<void>;\n pause(instanceId: string): Promise<void>;\n resume(instanceId: string): Promise<void>;\n cancel(instanceId: string): Promise<void>;\n}\n/**\n * Definition for a workflow handler\n */\nexport interface WorkflowDefinition<TParams = unknown, TResult = unknown> {\n /** Unique name for this workflow (kebab-case, e.g., \"order-fulfillment\") */\n name: string;\n /** Human-readable description of what this workflow does */\n description?: string;\n /** The workflow handler function */\n handler: (ctx: WorkflowContext<TParams>) => Promise<TResult>;\n /** Whether the workflow is enabled (default: true) */\n enabled?: boolean;\n}\n/**\n * Context passed to workflow handlers\n */\nexport interface WorkflowContext<TParams = unknown> {\n /** Environment bindings */\n env: Env;\n /** Unique instance ID for this workflow execution */\n instanceId: string;\n /** Parameters passed when workflow was started */\n params: TParams;\n /** Step utilities for durable execution */\n step: WorkflowStepUtilities;\n /** Integration client for external service calls */\n integrations: IntegrationClient;\n /** Logger for structured workflow logging */\n logger: WorkflowLogger;\n}\n/**\n * Step utilities for durable workflow execution\n */\nexport interface WorkflowStepUtilities {\n /** Execute a step with automatic retry and persistence */\n do<T>(name: string, fn: () => Promise<T>): Promise<T>;\n do<T>(name: string, options: StepOptions, fn: () => Promise<T>): Promise<T>;\n /** Sleep for a duration (e.g., '5 minutes', '1 hour', '24 hours') */\n sleep(name: string, duration: string): Promise<void>;\n /** Sleep until a specific timestamp */\n sleepUntil(name: string, timestamp: Date): Promise<void>;\n /** Wait for an external event */\n waitForEvent<T = unknown>(name: string, options?: WaitEventOptions): Promise<T>;\n}\n/**\n * Options for step execution\n */\nexport interface StepOptions {\n retries?: {\n limit: number;\n delay: string;\n backoff?: 'constant' | 'linear' | 'exponential';\n };\n timeout?: string;\n}\n/**\n * Options for waitForEvent\n */\nexport interface WaitEventOptions {\n type?: string;\n timeout?: string;\n}\n/**\n * Logger interface for workflow execution\n */\nexport interface WorkflowLogger {\n info(message: string, data?: Record<string, unknown>): void;\n warn(message: string, data?: Record<string, unknown>): void;\n error(message: string, data?: Record<string, unknown>): void;\n}\n/**\n * State of a registered workflow\n */\nexport interface WorkflowState {\n name: string;\n description?: string;\n enabled: boolean;\n activeInstances: number;\n totalRuns: number;\n lastRun: number | null;\n lastError?: string;\n}\n/**\n * Response from workflow status API\n */\nexport interface WorkflowStatusResponse {\n workflows: WorkflowState[];\n}\n",
|
|
39
|
-
"workflows.d.ts": "import type { WorkflowDefinition } from './core-workflow-types';\n/**\n * APP_WORKFLOWS - runtime accessor for registered workflow definitions.\n * Used by internal dynamic imports in core-workflow-cloudflare and core-workflow-instance.\n * This is a getter-backed constant so it always reflects the current registry state.\n */\nexport declare const APP_WORKFLOWS: WorkflowDefinition[];\nexport { triggerWorkflow, sendWorkflowEvent, getWorkflowStatus, workflowRoutes, } from './core-workflows';\nexport type { WorkflowResult, WorkflowStatus, WorkflowInstanceInfo, WorkflowDefinition, WorkflowContext, WorkflowStepUtilities, StepOptions, WaitEventOptions, WorkflowLogger, WorkflowState, WorkflowStatusResponse, } from './core-workflows';\nexport type { NativeWorkflowStatus, NativeStepStatus, NativeStepType, NativeWorkflowError, NativeStepAttempt, NativeStepExecution, NativeWorkflowInstance, NativeWorkflowConfig, NativeStepContext, WorkflowIndexEntry, ListWorkflowsOptions, ExecuteStepRequest, ExecuteStepResponse, PendingEvent, ReceivedEvent, WorkflowInstanceStub, WorkflowCoordinatorStub, } from './core-workflow-types';\nexport { DEFAULT_WORKFLOW_CONFIG } from './core-workflow-types';\nexport { WORKFLOW_INFRA_MODE } from './core-workflow-config';\nexport { WorkflowInstanceDO } from './core-workflow-instance';\nexport { WorkflowCoordinator } from './core-workflow-coordinator';\n",
|
|
39
|
+
"workflows.d.ts": "import type { WorkflowDefinition } from './core-workflow-types';\n/**\n * APP_WORKFLOWS - runtime accessor for registered workflow definitions.\n * Used by internal dynamic imports in core-workflow-cloudflare and core-workflow-instance.\n * This is a getter-backed constant so it always reflects the current registry state.\n */\nexport declare const APP_WORKFLOWS: WorkflowDefinition[];\nexport { triggerWorkflow, sendWorkflowEvent, getWorkflowStatus, workflowRoutes, } from './core-workflows';\nexport type { WorkflowResult, WorkflowStatus, WorkflowInstanceInfo, WorkflowDefinition, WorkflowContext, WorkflowStepUtilities, StepOptions, WaitEventOptions, WorkflowLogger, WorkflowState, WorkflowStatusResponse, } from './core-workflows';\nexport type { NativeWorkflowStatus, NativeStepStatus, NativeStepType, NativeWorkflowError, NativeStepAttempt, NativeStepExecution, NativeWorkflowInstance, NativeWorkflowConfig, NativeStepContext, WorkflowIndexEntry, ListWorkflowsOptions, ExecuteStepRequest, ExecuteStepResponse, PendingEvent, ReceivedEvent, WorkflowInstanceStub, WorkflowCoordinatorStub, } from './core-workflow-types';\nexport { DEFAULT_WORKFLOW_CONFIG } from './core-workflow-types';\nexport { WORKFLOW_INFRA_MODE } from './core-workflow-config';\nexport { WorkflowInstanceDO } from './core-workflow-instance';\nexport { WorkflowCoordinator } from './core-workflow-coordinator';\nexport { WorkflowPausedError, isWorkflowControlSignal } from './core-workflow-instance';\n",
|
|
40
40
|
"core-endpoints.d.ts": "/**\n * Core Public Endpoints Framework\n * DO NOT MODIFY THIS FILE - You may break the project functionality\n *\n * This module provides the infrastructure for exposing public API endpoints\n * that external systems can consume with API key authentication or public access.\n */\nimport { type Env } from './core-utils';\nimport { z } from 'zod';\nimport type { Hono } from 'hono';\ntype ZodSchema = z.ZodType<any, any, any>;\n/**\n * Authentication type for public endpoints\n */\nexport type EndpointAuthType = 'apiKey' | 'public';\n/**\n * HTTP methods supported by endpoints\n */\nexport type EndpointMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n/**\n * Schema definition for endpoint validation\n */\nexport interface EndpointSchema<TQuery = unknown, TBody = unknown, TResponse = unknown> {\n /** Query parameters schema */\n query?: ZodSchema;\n /** Request body schema (for POST/PUT/PATCH) */\n body?: ZodSchema;\n /** Response schema for documentation */\n response?: ZodSchema;\n}\n/**\n * Context provided to endpoint handlers\n */\nexport interface EndpointContext<TQuery = Record<string, unknown>, TBody = unknown> {\n /** Environment bindings */\n env: Env;\n /** Original request object */\n request: Request;\n /** Validated query parameters */\n query: TQuery;\n /** Validated request body (null for GET/DELETE) */\n body: TBody | null;\n /** Path parameters extracted from URL */\n params: Record<string, string>;\n /** Authentication information */\n auth: EndpointAuthInfo;\n /** Request headers */\n headers: Headers;\n /** Logger for endpoint operations */\n logger: EndpointLogger;\n}\n/**\n * Authentication information passed to handlers\n */\nexport interface EndpointAuthInfo {\n /** Authentication type used */\n type: EndpointAuthType;\n /** API key ID (if authenticated with API key) */\n apiKeyId?: string;\n /** Scopes granted to the API key */\n scopes?: string[];\n /** Whether request is authenticated */\n authenticated: boolean;\n}\n/**\n * Logger for endpoint operations\n */\nexport interface EndpointLogger {\n info(message: string, data?: Record<string, unknown>): void;\n warn(message: string, data?: Record<string, unknown>): void;\n error(message: string, data?: Record<string, unknown>): void;\n}\n/**\n * Metadata for endpoint documentation\n */\nexport interface EndpointMeta {\n /** Human-readable description */\n description?: string;\n /** Tags for grouping in documentation */\n tags?: string[];\n /** Deprecated flag */\n deprecated?: boolean;\n /** Deprecation message */\n deprecationMessage?: string;\n}\n/**\n * Public endpoint definition\n */\nexport interface EndpointDefinition<TQuery = Record<string, unknown>, TBody = unknown, TResponse = unknown> {\n /** URL path (e.g., '/v1/todos', '/users/:id') */\n path: string;\n /** HTTP method */\n method: EndpointMethod;\n /** Authentication requirement */\n auth: EndpointAuthType;\n /** Validation schemas */\n schema?: EndpointSchema<TQuery, TBody, TResponse>;\n /** Request handler */\n handler: EndpointHandler<TQuery, TBody, TResponse>;\n /** Endpoint metadata for documentation */\n meta?: EndpointMeta;\n}\n/**\n * Endpoint handler function type\n */\nexport type EndpointHandler<TQuery = Record<string, unknown>, TBody = unknown, TResponse = unknown> = (ctx: EndpointContext<TQuery, TBody>) => Promise<TResponse>;\n/** Type guard to distinguish EndpointDefinition from function-based route registrars */\nexport declare function isEndpointDefinition(entry: unknown): entry is EndpointDefinition;\n/**\n * Internal: Parsed endpoint info from request headers\n */\nexport interface ParsedEndpointRequest {\n endpointId: string;\n appId: string;\n authType: EndpointAuthType;\n apiKeyId?: string;\n scopes?: string[];\n}\n/**\n * Create a JSON response\n */\nexport declare function jsonResponse<T>(data: T, status?: number): Response;\n/**\n * Create an error response\n */\nexport declare function errorResponse(message: string, status?: number, code?: string): Response;\n/**\n * Convert Zod schema to JSON Schema for documentation\n * This is a simplified conversion that handles common cases\n */\nexport declare function zodToJsonSchema(schema: ZodSchema | undefined): Record<string, unknown> | undefined;\n/**\n * Endpoint router that matches requests to handlers\n */\nexport declare class EndpointRouter {\n private endpoints;\n /**\n * Register endpoints\n */\n register(endpoints: EndpointDefinition<unknown, unknown, unknown>[]): void;\n /**\n * Find endpoint matching the request\n */\n findEndpoint(method: string, path: string): {\n endpoint: EndpointDefinition<unknown, unknown, unknown>;\n params: Record<string, string>;\n } | null;\n /**\n * Get all registered endpoints for documentation\n */\n getAllEndpoints(): EndpointDefinition<unknown, unknown, unknown>[];\n}\n/**\n * Mount an EndpointDefinition as a native Hono route.\n *\n * Registers the endpoint at its natural path using the correct HTTP method,\n * with Zod validation for query/body schemas and full EndpointContext support.\n * This allows EndpointDefinition entries in APP_ROUTES to work as first-class\n * Hono routes with middleware, streaming, and WebSocket support intact.\n */\nexport declare function mountEndpointAsHonoRoute(app: Hono<{\n Bindings: Env;\n}>, endpoint: EndpointDefinition): void;\n/**\n * Handle an incoming public endpoint request.\n * Called by the platform when routing requests to the app.\n */\nexport declare function handleEndpointRequest<TQuery = unknown, TBody = unknown, TResponse = unknown>(request: Request, env: Env, endpoints: EndpointDefinition<TQuery, TBody, TResponse>[]): Promise<Response>;\n/**\n * Custom error for endpoint handlers\n * Use this to return specific HTTP status codes\n */\nexport declare class EndpointError extends Error {\n status: number;\n code?: string | undefined;\n constructor(message: string, status?: number, code?: string | undefined);\n static badRequest(message: string): EndpointError;\n static unauthorized(message?: string): EndpointError;\n static forbidden(message?: string): EndpointError;\n static notFound(message?: string): EndpointError;\n static conflict(message: string): EndpointError;\n}\n/**\n * Convert endpoint definition to registration data for workspace\n */\nexport declare function endpointToRegistration<TQuery = unknown, TBody = unknown, TResponse = unknown>(endpoint: EndpointDefinition<TQuery, TBody, TResponse>): {\n path: string;\n method: EndpointMethod;\n auth: EndpointAuthType;\n description?: string;\n tags?: string[];\n schema?: {\n query?: Record<string, unknown>;\n body?: Record<string, unknown>;\n response?: Record<string, unknown>;\n };\n};\nexport {};\n",
|
|
41
41
|
"core-utils.d.ts": "/**\n * Core Utilities\n * DO NOT MODIFY THIS FILE - You may break the project functionality\n *\n * This module provides:\n * - Env type: Environment bindings for Cloudflare Workers\n * - API helpers: Response utilities for Hono routes\n *\n * Entity System: See core-entities.ts\n * Workspace Context: See core-workspace.ts\n * Scheduler: See core-scheduler.ts\n */\nimport type { Context } from 'hono';\nimport type { EntityDO } from './core-entity-do';\nimport type { SchedulerDO } from './core-scheduler';\nimport type { BaseAgent } from './core-base-agent';\n/**\n * Environment bindings for Cloudflare Workers\n */\nexport interface Env {\n EntityDO: DurableObjectNamespace<EntityDO>;\n SchedulerDO: DurableObjectNamespace<SchedulerDO>;\n BaseAgent?: DurableObjectNamespace<BaseAgent>;\n WorkspaceObject?: DurableObjectNamespace;\n WorkflowInstance: DurableObjectNamespace;\n WorkflowCoordinator: DurableObjectNamespace;\n BUCKET?: R2Bucket;\n WORKSPACE_ID?: string;\n APP_ID?: string;\n APP_NAME?: string;\n WORKSPACE_API_URL?: string;\n WORKSPACE_API_BASE_URL?: string;\n WORKSPACE_API_KEY?: string;\n DEPLOYMENT_MODE?: 'preview' | 'production';\n VITE_DEPLOYMENT_MODE?: 'preview' | 'production';\n RUNWORK_AI_PROXY_URL?: string;\n RUNWORK_PROXY_TOKEN?: string;\n INTEGRATIONS_PROXY_URL?: string;\n ALLOWED_ORIGINS?: string;\n}\n/**\n * Return a successful JSON response.\n * Returns the data directly as the response body with 200 status.\n */\nexport declare const ok: <T>(c: Context, data: T) => Response & import(\"hono\").TypedResponse<{\n [x: string]: import(\"hono/utils/types\").JSONValue;\n}, import(\"hono/utils/http-status\").ContentfulStatusCode, \"json\">;\n/**\n * Return a 400 Bad Request response\n * Automatically logs the error for debugging in production logs\n */\nexport declare const bad: (c: Context, error: string) => Response & import(\"hono\").TypedResponse<{\n error: string;\n}, 400, \"json\">;\n/**\n * Return a 404 Not Found response\n * Automatically logs the error for debugging in production logs\n */\nexport declare const notFound: (c: Context, error?: string) => Response & import(\"hono\").TypedResponse<{\n error: string;\n}, 404, \"json\">;\n/**\n * Type guard for non-empty strings\n */\nexport declare const isStr: (s: unknown) => s is string;\n/**\n * Safely clone a value for Durable Object storage.\n * Strips non-serializable references (R2Bucket, D1Database, DO stubs, etc.)\n * that would cause structured clone to fail in ctx.storage.put().\n */\nexport declare function safeClone<T>(value: T, fallback?: T): T;\nexport declare function platformFetch(env: Env, url: string | URL, init?: RequestInit): Promise<Response>;\n/**\n * Workspace API fetch - routes workspace service requests through WorkspaceObject DO\n * for production WfP workers, avoiding 522 recursive invocation errors.\n *\n * Workers for Platforms workers cannot HTTP-fetch back to their dispatcher domain.\n * This routes through the WorkspaceObject DO binding in production, and falls back\n * to standard HTTP with x-workspace-key auth header in preview/sandbox.\n *\n * @param env - Environment with workspace bindings\n * @param path - Workspace API sub-path (e.g., '/ingest-event')\n * @param init - Standard fetch options\n */\nexport declare function workspaceApiFetch(env: Pick<Env, 'WORKSPACE_API_URL' | 'WORKSPACE_API_KEY' | 'WORKSPACE_ID' | 'DEPLOYMENT_MODE' | 'WorkspaceObject'>, path: string, init?: RequestInit): Promise<Response>;\n",
|
|
42
42
|
"vite.d.ts": "import { defineConfig, type Plugin } from \"vite\";\nexport interface RunworkPluginOptions {\n /**\n * Override the detected mode. By default, the plugin detects sandbox mode\n * when DEPLOYMENT_MODE or WORKSPACE_ID environment variables are present.\n */\n mode?: \"sandbox\" | \"local\";\n /**\n * Root directory of the project. Defaults to process.cwd().\n */\n root?: string;\n /**\n * Path aliases to configure. Defaults to { \"@\": \"./src\", \"@shared\": \"./shared\" }.\n */\n aliases?: Record<string, string>;\n /**\n * Whether to use JSON logging (pino) in sandbox mode.\n * Defaults to true when VITE_LOGGER_TYPE=json.\n */\n jsonLogger?: boolean;\n}\nexport declare function runwork(options?: RunworkPluginOptions): ReturnType<typeof defineConfig>;\nexport type { Plugin };\n",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.13.
|
|
1
|
+
export declare const VERSION = "0.13.4";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/embed-types.ts -- do not edit
|
|
2
|
-
export const VERSION = "0.13.
|
|
2
|
+
export const VERSION = "0.13.4";
|
|
@@ -3,7 +3,7 @@ import { execFileSync } from 'child_process';
|
|
|
3
3
|
import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync, mkdirSync, } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { tmpdir } from 'os';
|
|
6
|
-
import { isIgnored } from '../auto-commit.js';
|
|
6
|
+
import { isIgnored, commitWorkingTree } from '../auto-commit.js';
|
|
7
7
|
const tempDirs = [];
|
|
8
8
|
function makeTempDir(prefix) {
|
|
9
9
|
const dir = mkdtempSync(join(tmpdir(), `runwork-autocommit-test-${prefix}-`));
|
|
@@ -350,6 +350,64 @@ describe('real-life auto-commit scenarios', () => {
|
|
|
350
350
|
const staged = execFileSync('git', ['diff', '--cached', '--name-only'], { cwd: local, encoding: 'utf-8' }).trim();
|
|
351
351
|
expect(staged).toBe('');
|
|
352
352
|
});
|
|
353
|
+
it('commitWorkingTree stages and commits new untracked files (deploy path)', () => {
|
|
354
|
+
// Deploy must commit pending working-tree changes even when no dev
|
|
355
|
+
// session ever ran -- including brand-new untracked files.
|
|
356
|
+
const { local } = createTestRepo();
|
|
357
|
+
commitFile(local, 'init.txt', 'hello', 'init');
|
|
358
|
+
writeFileSync(join(local, 'new-feature.ts'), 'export const feature = true;');
|
|
359
|
+
mkdirSync(join(local, 'src'));
|
|
360
|
+
writeFileSync(join(local, 'src', 'page.tsx'), 'export default () => null;');
|
|
361
|
+
const result = commitWorkingTree(local, 'deploy: test');
|
|
362
|
+
expect(result.committed).toBe(true);
|
|
363
|
+
expect(result.fileCount).toBe(2);
|
|
364
|
+
const log = execFileSync('git', ['log', '-1', '--format=%s'], { cwd: local, encoding: 'utf-8' }).trim();
|
|
365
|
+
expect(log).toBe('deploy: test');
|
|
366
|
+
const status = execFileSync('git', ['status', '--porcelain'], { cwd: local, encoding: 'utf-8' }).trim();
|
|
367
|
+
expect(status).toBe('');
|
|
368
|
+
});
|
|
369
|
+
it('commitWorkingTree stages tracked modifications and deletions', () => {
|
|
370
|
+
const { local } = createTestRepo();
|
|
371
|
+
commitFile(local, 'keep.ts', 'export const a = 1;', 'init');
|
|
372
|
+
commitFile(local, 'remove.ts', 'export const b = 2;', 'add remove');
|
|
373
|
+
writeFileSync(join(local, 'keep.ts'), 'export const a = 99;');
|
|
374
|
+
rmSync(join(local, 'remove.ts'));
|
|
375
|
+
const result = commitWorkingTree(local, 'deploy: edits');
|
|
376
|
+
expect(result.committed).toBe(true);
|
|
377
|
+
expect(result.fileCount).toBe(2);
|
|
378
|
+
expect(existsSync(join(local, 'remove.ts'))).toBe(false);
|
|
379
|
+
});
|
|
380
|
+
it('commitWorkingTree returns committed=false on a clean tree', () => {
|
|
381
|
+
const { local } = createTestRepo();
|
|
382
|
+
commitFile(local, 'init.txt', 'hello', 'init');
|
|
383
|
+
const result = commitWorkingTree(local, 'deploy: noop');
|
|
384
|
+
expect(result.committed).toBe(false);
|
|
385
|
+
expect(result.fileCount).toBe(0);
|
|
386
|
+
const count = execFileSync('git', ['rev-list', '--count', 'HEAD'], { cwd: local, encoding: 'utf-8' }).trim();
|
|
387
|
+
expect(count).toBe('1');
|
|
388
|
+
});
|
|
389
|
+
it('commitWorkingTree does NOT stage untracked secrets / ignored dirs (matches dev, not git add -A)', () => {
|
|
390
|
+
// SECURITY: a bare `.env` is not in the runwork-app .gitignore, so a
|
|
391
|
+
// blanket `git add -A` would commit and push it. commitWorkingTree must
|
|
392
|
+
// filter untracked files through the CLI ignore matcher, exactly like
|
|
393
|
+
// the dev watcher, so local secrets never reach the remote.
|
|
394
|
+
const { local } = createTestRepo();
|
|
395
|
+
commitFile(local, 'init.txt', 'hello', 'init');
|
|
396
|
+
// Real source that SHOULD be committed.
|
|
397
|
+
writeFileSync(join(local, 'app.ts'), 'export const x = 1;');
|
|
398
|
+
// Secrets / caches that MUST NOT be committed.
|
|
399
|
+
writeFileSync(join(local, '.env'), 'SECRET=do_not_leak');
|
|
400
|
+
writeFileSync(join(local, '.dev.vars'), 'WORKSPACE_API_KEY=do_not_leak');
|
|
401
|
+
mkdirSync(join(local, '.bun-cache'));
|
|
402
|
+
writeFileSync(join(local, '.bun-cache', 'pkg.json'), '{}');
|
|
403
|
+
const result = commitWorkingTree(local, 'deploy: test');
|
|
404
|
+
expect(result.committed).toBe(true);
|
|
405
|
+
const tracked = execFileSync('git', ['ls-files'], { cwd: local, encoding: 'utf-8' }).trim().split('\n');
|
|
406
|
+
expect(tracked).toContain('app.ts');
|
|
407
|
+
expect(tracked).not.toContain('.env');
|
|
408
|
+
expect(tracked).not.toContain('.dev.vars');
|
|
409
|
+
expect(tracked.some((f) => f.startsWith('.bun-cache/'))).toBe(false);
|
|
410
|
+
});
|
|
353
411
|
it('push -u sets upstream on first push', () => {
|
|
354
412
|
// commitAndPush tries `git rev-parse --abbrev-ref @{u}` to check
|
|
355
413
|
// for upstream. If it fails, it uses `push -u` to set upstream.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { isDirtyOrAhead, shouldWarnBeforeDeploy, } from '../deploy-guard.js';
|
|
3
|
+
import { SESSION_FILE_SCHEMA_VERSION, } from '../../dev/session.js';
|
|
4
|
+
function makeSessionFile(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
version: SESSION_FILE_SCHEMA_VERSION,
|
|
7
|
+
pid: process.pid,
|
|
8
|
+
sessionId: 'sess_test',
|
|
9
|
+
appId: 'test-app',
|
|
10
|
+
previewUrl: 'https://test.preview.runwork.dev',
|
|
11
|
+
startedAt: 1_700_000_000_000,
|
|
12
|
+
bootTime: 1_600_000_000_000,
|
|
13
|
+
cliVersion: '0.10.2',
|
|
14
|
+
mode: 'foreground',
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const noSession = { state: 'none' };
|
|
19
|
+
const aliveSyncing = { state: 'alive', file: makeSessionFile({ noSync: false }) };
|
|
20
|
+
const aliveNoSync = { state: 'alive', file: makeSessionFile({ noSync: true }) };
|
|
21
|
+
const aliveLegacy = { state: 'alive', file: makeSessionFile() }; // noSync undefined
|
|
22
|
+
const staleSession = { state: 'stale', reason: 'pid-dead', file: makeSessionFile() };
|
|
23
|
+
const dirty = { dirty: true, ahead: false };
|
|
24
|
+
const ahead = { dirty: false, ahead: true };
|
|
25
|
+
const clean = { dirty: false, ahead: false };
|
|
26
|
+
describe('isDirtyOrAhead()', () => {
|
|
27
|
+
it('is true when dirty', () => {
|
|
28
|
+
expect(isDirtyOrAhead(dirty)).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
it('is true when ahead', () => {
|
|
31
|
+
expect(isDirtyOrAhead(ahead)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
it('is false when clean and not ahead', () => {
|
|
34
|
+
expect(isDirtyOrAhead(clean)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('shouldWarnBeforeDeploy()', () => {
|
|
38
|
+
it('warns: dirty tree + no session', () => {
|
|
39
|
+
expect(shouldWarnBeforeDeploy(dirty, noSession)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it('does not warn: dirty tree + active auto-syncing session', () => {
|
|
42
|
+
expect(shouldWarnBeforeDeploy(dirty, aliveSyncing)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('does not warn: dirty tree + active legacy session (noSync undefined treated as syncing)', () => {
|
|
45
|
+
expect(shouldWarnBeforeDeploy(dirty, aliveLegacy)).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it('warns: dirty tree + active no-sync session', () => {
|
|
48
|
+
expect(shouldWarnBeforeDeploy(dirty, aliveNoSync)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it('warns: ahead tree + no session', () => {
|
|
51
|
+
expect(shouldWarnBeforeDeploy(ahead, noSession)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
it('warns: dirty tree + stale session (watcher not actually running)', () => {
|
|
54
|
+
expect(shouldWarnBeforeDeploy(dirty, staleSession)).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it('does not warn: clean + not ahead, regardless of session', () => {
|
|
57
|
+
expect(shouldWarnBeforeDeploy(clean, noSession)).toBe(false);
|
|
58
|
+
expect(shouldWarnBeforeDeploy(clean, aliveSyncing)).toBe(false);
|
|
59
|
+
expect(shouldWarnBeforeDeploy(clean, aliveNoSync)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -10,4 +10,26 @@ export declare function watchAndAutoCommit(directory: string, client: ApiClient,
|
|
|
10
10
|
onFastSync?: (count: number) => void;
|
|
11
11
|
onGitPush?: (count: number) => void;
|
|
12
12
|
}): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Stage and commit the current working tree in a single shot, staging exactly
|
|
15
|
+
* what the dev watcher would: tracked modifications/deletions (`git add -u`)
|
|
16
|
+
* plus untracked files that the CLI ignore matcher does NOT exclude.
|
|
17
|
+
*
|
|
18
|
+
* It deliberately does NOT use `git add -A`. A blanket add honors only
|
|
19
|
+
* `.gitignore`, which in the runwork-app template excludes `.env.*` but not a
|
|
20
|
+
* bare `.env` (vibe-apps write both `.dev.vars` and `.env` locally) — so
|
|
21
|
+
* `git add -A` would commit and push local secrets. The CLI ignore matcher
|
|
22
|
+
* (the same one the dev watcher uses) excludes `.env`, `.dev.vars`,
|
|
23
|
+
* `.bun-cache`, `.runwork`, and `*.log`, keeping deploy's staging in parity
|
|
24
|
+
* with dev.
|
|
25
|
+
*
|
|
26
|
+
* Used by `runwork deploy`, which must commit pending working-tree changes
|
|
27
|
+
* even when no dev session ever ran.
|
|
28
|
+
*
|
|
29
|
+
* Returns the number of files committed (0 when nothing was staged).
|
|
30
|
+
*/
|
|
31
|
+
export declare function commitWorkingTree(cwd: string, message: string): {
|
|
32
|
+
committed: boolean;
|
|
33
|
+
fileCount: number;
|
|
34
|
+
};
|
|
13
35
|
export declare function stopAutoCommit(): Promise<void>;
|
package/dist/git/auto-commit.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync } from 'fs';
|
|
|
3
3
|
import { watch } from 'chokidar';
|
|
4
4
|
import { join, relative } from 'path';
|
|
5
5
|
import { dim, cyan, yellow } from '../ui/colors.js';
|
|
6
|
-
import { buildIgnoreSets, defaultIgnoreSets, isPathIgnored } from '../utils/ignore-matcher.js';
|
|
6
|
+
import { buildIgnoreSets, defaultIgnoreSets, isPathIgnored, isRelPathIgnored } from '../utils/ignore-matcher.js';
|
|
7
7
|
let watcher = null;
|
|
8
8
|
let fastSyncTimer = null;
|
|
9
9
|
let gitTimer = null;
|
|
@@ -125,6 +125,68 @@ async function executeFastSync(directory, client, appId) {
|
|
|
125
125
|
// ========================================
|
|
126
126
|
// GIT PATH (2s debounce)
|
|
127
127
|
// ========================================
|
|
128
|
+
/**
|
|
129
|
+
* Stage and commit the current working tree in a single shot, staging exactly
|
|
130
|
+
* what the dev watcher would: tracked modifications/deletions (`git add -u`)
|
|
131
|
+
* plus untracked files that the CLI ignore matcher does NOT exclude.
|
|
132
|
+
*
|
|
133
|
+
* It deliberately does NOT use `git add -A`. A blanket add honors only
|
|
134
|
+
* `.gitignore`, which in the runwork-app template excludes `.env.*` but not a
|
|
135
|
+
* bare `.env` (vibe-apps write both `.dev.vars` and `.env` locally) — so
|
|
136
|
+
* `git add -A` would commit and push local secrets. The CLI ignore matcher
|
|
137
|
+
* (the same one the dev watcher uses) excludes `.env`, `.dev.vars`,
|
|
138
|
+
* `.bun-cache`, `.runwork`, and `*.log`, keeping deploy's staging in parity
|
|
139
|
+
* with dev.
|
|
140
|
+
*
|
|
141
|
+
* Used by `runwork deploy`, which must commit pending working-tree changes
|
|
142
|
+
* even when no dev session ever ran.
|
|
143
|
+
*
|
|
144
|
+
* Returns the number of files committed (0 when nothing was staged).
|
|
145
|
+
*/
|
|
146
|
+
export function commitWorkingTree(cwd, message) {
|
|
147
|
+
// Tracked modifications and deletions.
|
|
148
|
+
execFileSync('git', ['add', '-u'], { cwd, stdio: 'pipe' });
|
|
149
|
+
// Untracked files that git itself doesn't ignore (.gitignore), further
|
|
150
|
+
// filtered through the CLI ignore matcher to match the dev watcher exactly.
|
|
151
|
+
const ignoreSets = buildIgnoreSets(cwd);
|
|
152
|
+
const untracked = execFileSync('git', ['-c', 'core.quotePath=false', 'ls-files', '--others', '--exclude-standard'], { cwd, encoding: 'utf-8' }).trim();
|
|
153
|
+
if (untracked) {
|
|
154
|
+
const toAdd = untracked
|
|
155
|
+
.split('\n')
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.filter((p) => !isRelPathIgnored(p, ignoreSets));
|
|
158
|
+
const BATCH_SIZE = 50;
|
|
159
|
+
for (let i = 0; i < toAdd.length; i += BATCH_SIZE) {
|
|
160
|
+
const batch = toAdd.slice(i, i + BATCH_SIZE);
|
|
161
|
+
if (batch.length === 0)
|
|
162
|
+
continue;
|
|
163
|
+
try {
|
|
164
|
+
execFileSync('git', ['add', '--', ...batch], { cwd, stdio: 'pipe' });
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// A file may have vanished between listing and staging.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return commitStaged(cwd, message);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Commit whatever is currently staged. Shared by the deploy one-shot path
|
|
175
|
+
* (after `git add -A`) and the dev watcher path (after a selective add) so
|
|
176
|
+
* the "is anything staged? then commit" logic lives in one place.
|
|
177
|
+
*
|
|
178
|
+
* Returns the number of files committed (0 when nothing was staged).
|
|
179
|
+
*/
|
|
180
|
+
function commitStaged(cwd, message) {
|
|
181
|
+
// An empty staging area means there is nothing to commit.
|
|
182
|
+
const staged = execFileSync('git', ['-c', 'core.quotePath=false', 'diff', '--cached', '--name-only'], { cwd, encoding: 'utf-8' });
|
|
183
|
+
if (!staged.trim()) {
|
|
184
|
+
return { committed: false, fileCount: 0 };
|
|
185
|
+
}
|
|
186
|
+
const stagedFiles = staged.trim().split('\n').filter(Boolean);
|
|
187
|
+
execFileSync('git', ['commit', '-m', message], { cwd, stdio: 'pipe' });
|
|
188
|
+
return { committed: true, fileCount: stagedFiles.length };
|
|
189
|
+
}
|
|
128
190
|
function scheduleGitCommit() {
|
|
129
191
|
if (gitPushing) {
|
|
130
192
|
gitPendingAfterPush = true;
|
|
@@ -158,14 +220,11 @@ function commitAndPush() {
|
|
|
158
220
|
}
|
|
159
221
|
}
|
|
160
222
|
}
|
|
161
|
-
//
|
|
162
|
-
const staged = execFileSync('git', ['-c', 'core.quotePath=false', 'diff', '--cached', '--name-only'], { encoding: 'utf-8' });
|
|
163
|
-
if (!staged.trim())
|
|
164
|
-
return;
|
|
165
|
-
const stagedFiles = staged.trim().split('\n').filter(Boolean);
|
|
166
|
-
// Commit
|
|
223
|
+
// Commit whatever ended up staged (shared with the deploy path).
|
|
167
224
|
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
168
|
-
|
|
225
|
+
const { committed, fileCount } = commitStaged(process.cwd(), `dev: updated ${timestamp}`);
|
|
226
|
+
if (!committed)
|
|
227
|
+
return;
|
|
169
228
|
// Pull --rebase to integrate any remote changes, then push
|
|
170
229
|
try {
|
|
171
230
|
execFileSync('git', ['pull', '--rebase', 'runwork', 'main'], { stdio: 'pipe' });
|
|
@@ -184,7 +243,9 @@ function commitAndPush() {
|
|
|
184
243
|
execFileSync('git', ['merge', '--abort'], { stdio: 'pipe' });
|
|
185
244
|
}
|
|
186
245
|
catch { /* no merge in progress */ }
|
|
187
|
-
console.warn(yellow('Auto-sync:
|
|
246
|
+
console.warn(yellow('Auto-sync: could not reconcile with the remote (conflicting changes). Your work is committed locally and safe.'));
|
|
247
|
+
console.warn(dim(' Resolve it by running: git fetch runwork && git rebase runwork/main'));
|
|
248
|
+
console.warn(dim(' Fix any conflicts (git status), then restart `runwork dev` to resume auto-sync.'));
|
|
188
249
|
return;
|
|
189
250
|
}
|
|
190
251
|
}
|
|
@@ -201,8 +262,8 @@ function commitAndPush() {
|
|
|
201
262
|
catch {
|
|
202
263
|
execFileSync('git', ['push', '-u', 'runwork', 'HEAD:main'], { stdio: 'pipe' });
|
|
203
264
|
}
|
|
204
|
-
console.log(dim(` Pushed ${
|
|
205
|
-
activeCallbacks?.onGitPush?.(
|
|
265
|
+
console.log(dim(` Pushed ${fileCount} file(s) to git.`));
|
|
266
|
+
activeCallbacks?.onGitPush?.(fileCount);
|
|
206
267
|
}
|
|
207
268
|
catch (error) {
|
|
208
269
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files that must survive a remote sync. The sync path can rebase or, as a
|
|
3
|
+
* last resort, merge with `-X theirs` (remote wins) to reconcile diverged
|
|
4
|
+
* histories. If the remote tip is missing one of these (e.g. right after a
|
|
5
|
+
* fresh init, or a server-side reconcile), the merge can delete the local
|
|
6
|
+
* copy. Losing `.runwork.json` in particular bricks the app for the CLI
|
|
7
|
+
* ("not a Runwork app"). We snapshot these before the sync and restore any
|
|
8
|
+
* the sync removed.
|
|
9
|
+
*
|
|
10
|
+
* Shared by `runwork dev` (startup sync) and `runwork deploy` so both protect
|
|
11
|
+
* the same set with identical logic.
|
|
12
|
+
*/
|
|
13
|
+
export declare const CRITICAL_FILES: readonly [".runwork.json", "blueprint.json", ".gitignore"];
|
|
14
|
+
export interface CriticalSnapshot {
|
|
15
|
+
path: string;
|
|
16
|
+
contents: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function snapshotCriticalFiles(cwd: string): CriticalSnapshot[];
|
|
19
|
+
export declare function restoreMissingCriticalFiles(cwd: string, snapshots: CriticalSnapshot[]): string[];
|
|
20
|
+
export declare function commitAndPushRestoredFiles(cwd: string, files: string[]): boolean;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { execFileSync } from '../utils/subprocess.js';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
/**
|
|
5
|
+
* Files that must survive a remote sync. The sync path can rebase or, as a
|
|
6
|
+
* last resort, merge with `-X theirs` (remote wins) to reconcile diverged
|
|
7
|
+
* histories. If the remote tip is missing one of these (e.g. right after a
|
|
8
|
+
* fresh init, or a server-side reconcile), the merge can delete the local
|
|
9
|
+
* copy. Losing `.runwork.json` in particular bricks the app for the CLI
|
|
10
|
+
* ("not a Runwork app"). We snapshot these before the sync and restore any
|
|
11
|
+
* the sync removed.
|
|
12
|
+
*
|
|
13
|
+
* Shared by `runwork dev` (startup sync) and `runwork deploy` so both protect
|
|
14
|
+
* the same set with identical logic.
|
|
15
|
+
*/
|
|
16
|
+
export const CRITICAL_FILES = ['.runwork.json', 'blueprint.json', '.gitignore'];
|
|
17
|
+
export function snapshotCriticalFiles(cwd) {
|
|
18
|
+
const snapshots = [];
|
|
19
|
+
for (const rel of CRITICAL_FILES) {
|
|
20
|
+
const abs = join(cwd, rel);
|
|
21
|
+
if (!existsSync(abs))
|
|
22
|
+
continue;
|
|
23
|
+
try {
|
|
24
|
+
snapshots.push({ path: rel, contents: readFileSync(abs, 'utf-8') });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Best-effort: skip unreadable files.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return snapshots;
|
|
31
|
+
}
|
|
32
|
+
export function restoreMissingCriticalFiles(cwd, snapshots) {
|
|
33
|
+
const restored = [];
|
|
34
|
+
for (const snap of snapshots) {
|
|
35
|
+
const abs = join(cwd, snap.path);
|
|
36
|
+
if (existsSync(abs))
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
writeFileSync(abs, snap.contents, 'utf-8');
|
|
40
|
+
restored.push(snap.path);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Best-effort: skip files we cannot write back.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return restored;
|
|
47
|
+
}
|
|
48
|
+
export function commitAndPushRestoredFiles(cwd, files) {
|
|
49
|
+
if (files.length === 0)
|
|
50
|
+
return false;
|
|
51
|
+
try {
|
|
52
|
+
execFileSync('git', ['add', '--', ...files], { cwd, stdio: 'pipe' });
|
|
53
|
+
execFileSync('git', ['commit', '-m', 'chore: restore critical files removed by sync'], { cwd, stdio: 'pipe' });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
// `HEAD:main` so the push works regardless of local branch name.
|
|
60
|
+
execFileSync('git', ['push', 'runwork', 'HEAD:main'], { cwd, stdio: 'pipe' });
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Push failures are non-fatal: the local copy is restored, and the
|
|
65
|
+
// restoration commit will be pushed with the next auto-sync cycle.
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|