runwork 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/agents/__tests__/claude-code-stats.test.d.ts +1 -0
  2. package/dist/agents/__tests__/claude-code-stats.test.js +153 -0
  3. package/dist/agents/__tests__/claude-desktop-stats.test.d.ts +1 -0
  4. package/dist/agents/__tests__/claude-desktop-stats.test.js +280 -0
  5. package/dist/agents/__tests__/codex-stats.test.d.ts +1 -0
  6. package/dist/agents/__tests__/codex-stats.test.js +262 -0
  7. package/dist/agents/__tests__/cursor-stats.test.d.ts +1 -0
  8. package/dist/agents/__tests__/cursor-stats.test.js +183 -0
  9. package/dist/agents/__tests__/graceful-degradation.test.d.ts +1 -0
  10. package/dist/agents/__tests__/graceful-degradation.test.js +123 -0
  11. package/dist/agents/__tests__/instruction-hint.test.d.ts +1 -0
  12. package/dist/agents/__tests__/instruction-hint.test.js +69 -0
  13. package/dist/agents/__tests__/intro-skill.test.d.ts +1 -0
  14. package/dist/agents/__tests__/intro-skill.test.js +135 -0
  15. package/dist/agents/__tests__/schema-validation.test.d.ts +1 -0
  16. package/dist/agents/__tests__/schema-validation.test.js +212 -0
  17. package/dist/agents/claude-code.d.ts +16 -1
  18. package/dist/agents/claude-code.js +344 -21
  19. package/dist/agents/claude-desktop.d.ts +10 -1
  20. package/dist/agents/claude-desktop.js +250 -8
  21. package/dist/agents/cline.d.ts +13 -0
  22. package/dist/agents/cline.js +88 -0
  23. package/dist/agents/codex.d.ts +7 -2
  24. package/dist/agents/codex.js +130 -18
  25. package/dist/agents/cursor.d.ts +6 -1
  26. package/dist/agents/cursor.js +164 -7
  27. package/dist/agents/detect.js +30 -7
  28. package/dist/agents/gemini.d.ts +6 -3
  29. package/dist/agents/gemini.js +57 -20
  30. package/dist/agents/generic-adapter.d.ts +23 -0
  31. package/dist/agents/generic-adapter.js +106 -0
  32. package/dist/agents/intro-skill.d.ts +36 -0
  33. package/dist/agents/intro-skill.js +358 -0
  34. package/dist/agents/registry.d.ts +53 -0
  35. package/dist/agents/registry.js +219 -0
  36. package/dist/agents/types.d.ts +44 -1
  37. package/dist/agents/types.js +11 -4
  38. package/dist/agents/utils/instruction-hint.d.ts +11 -0
  39. package/dist/agents/utils/instruction-hint.js +62 -0
  40. package/dist/agents/utils/json-config.js +3 -3
  41. package/dist/agents/windsurf.d.ts +2 -0
  42. package/dist/agents/windsurf.js +26 -9
  43. package/dist/api/client.d.ts +85 -2
  44. package/dist/api/client.js +40 -10
  45. package/dist/auth/__tests__/login-flow.test.d.ts +1 -0
  46. package/dist/auth/__tests__/login-flow.test.js +125 -0
  47. package/dist/auth/login-flow.d.ts +16 -0
  48. package/dist/auth/login-flow.js +101 -1
  49. package/dist/commands/clone.d.ts +9 -1
  50. package/dist/commands/clone.js +40 -12
  51. package/dist/commands/deploy.js +27 -4
  52. package/dist/commands/dev.js +35 -1
  53. package/dist/commands/doctor.d.ts +2 -0
  54. package/dist/commands/doctor.js +82 -0
  55. package/dist/commands/info.js +6 -2
  56. package/dist/commands/init.d.ts +10 -2
  57. package/dist/commands/init.js +58 -15
  58. package/dist/commands/integrations.js +47 -10
  59. package/dist/commands/login.js +31 -4
  60. package/dist/commands/mcp.js +143 -15
  61. package/dist/commands/setup.js +47 -122
  62. package/dist/commands/skills.js +230 -12
  63. package/dist/commands/sync.d.ts +8 -0
  64. package/dist/commands/sync.js +398 -65
  65. package/dist/commands/welcome.js +6 -3
  66. package/dist/generated/version.d.ts +1 -1
  67. package/dist/generated/version.js +1 -1
  68. package/dist/health/__tests__/checks.test.d.ts +1 -0
  69. package/dist/health/__tests__/checks.test.js +223 -0
  70. package/dist/health/checks.d.ts +20 -0
  71. package/dist/health/checks.js +392 -0
  72. package/dist/health/runner.d.ts +2 -0
  73. package/dist/health/runner.js +26 -0
  74. package/dist/health/types.d.ts +12 -0
  75. package/dist/health/types.js +1 -0
  76. package/dist/index.js +32 -0
  77. package/dist/sync/__tests__/change-detect.test.d.ts +1 -0
  78. package/dist/sync/__tests__/change-detect.test.js +123 -0
  79. package/dist/sync/__tests__/hash.test.d.ts +1 -0
  80. package/dist/sync/__tests__/hash.test.js +18 -0
  81. package/dist/sync/change-detect.d.ts +19 -0
  82. package/dist/sync/change-detect.js +136 -0
  83. package/dist/sync/conflict-ui.d.ts +5 -0
  84. package/dist/sync/conflict-ui.js +78 -0
  85. package/dist/sync/executor.d.ts +21 -0
  86. package/dist/sync/executor.js +98 -0
  87. package/dist/sync/hash.d.ts +1 -0
  88. package/dist/sync/hash.js +4 -0
  89. package/dist/sync/types.d.ts +24 -0
  90. package/dist/sync/types.js +1 -0
  91. package/dist/types.d.ts +12 -0
  92. package/dist/ui/__tests__/banner.test.js +17 -1
  93. package/dist/ui/banner.d.ts +1 -1
  94. package/dist/ui/banner.js +27 -2
  95. package/dist/utils/__tests__/resolve.test.d.ts +1 -0
  96. package/dist/utils/__tests__/resolve.test.js +95 -0
  97. package/dist/utils/__tests__/sqlite.test.d.ts +1 -0
  98. package/dist/utils/__tests__/sqlite.test.js +77 -0
  99. package/dist/utils/agent-guidance.d.ts +32 -0
  100. package/dist/utils/agent-guidance.js +106 -0
  101. package/dist/utils/fs.js +2 -2
  102. package/dist/utils/help-json.d.ts +56 -0
  103. package/dist/utils/help-json.js +85 -0
  104. package/dist/utils/resolve.d.ts +12 -0
  105. package/dist/utils/resolve.js +52 -0
  106. package/dist/utils/setup-state.d.ts +3 -0
  107. package/dist/utils/setup-state.js +19 -0
  108. package/dist/utils/sqlite.d.ts +7 -0
  109. package/dist/utils/sqlite.js +24 -0
  110. package/dist/utils/which.d.ts +6 -0
  111. package/dist/utils/which.js +18 -0
  112. package/package.json +7 -3
  113. package/dist/agents/copilot-cli.d.ts +0 -10
  114. package/dist/agents/copilot-cli.js +0 -46
  115. package/dist/agents/copilot-vscode.d.ts +0 -10
  116. package/dist/agents/copilot-vscode.js +0 -37
@@ -18,7 +18,7 @@ export declare function getKeyboardHints(): string;
18
18
  * Interactive wizard that asks which AI agent the user wants to use,
19
19
  * then shows specific instructions for opening this app in that agent.
20
20
  */
21
- export declare function runAgentWizard(directory: string): Promise<void>;
21
+ export declare function runAgentWizard(directory: string, configuredAgents?: string[]): Promise<void>;
22
22
  /**
23
23
  * Returns full info panel (for 'i' key in dev mode) showing
24
24
  * workspace, app, preview URL, and agent guidance.
package/dist/ui/banner.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { basename } from 'path';
1
2
  import { bold, cyan, dim, green } from './colors.js';
2
3
  export const SUPPORTED_AGENTS = [
3
4
  { id: 'claude-code', name: 'Claude Code', command: 'cd {dir} && claude' },
@@ -48,7 +49,32 @@ export function getKeyboardHints() {
48
49
  * Interactive wizard that asks which AI agent the user wants to use,
49
50
  * then shows specific instructions for opening this app in that agent.
50
51
  */
51
- export async function runAgentWizard(directory) {
52
+ export async function runAgentWizard(directory, configuredAgents) {
53
+ // Skip interactive wizard in non-TTY environments (CI, agents)
54
+ if (!process.stdout.isTTY) {
55
+ console.log(getAgentGuidance(directory));
56
+ return;
57
+ }
58
+ const slug = basename(directory) || directory;
59
+ // If we already know which agents are configured, skip the prompt
60
+ if (configuredAgents && configuredAgents.length > 0) {
61
+ const knownAgent = SUPPORTED_AGENTS.find(a => configuredAgents.includes(a.id));
62
+ if (knownAgent) {
63
+ const agentCmd = knownAgent.command.replace(/{dir}/g, slug);
64
+ console.log('');
65
+ console.log(bold('Get started:'));
66
+ console.log('');
67
+ console.log(` ${dim('1.')} cd ${slug} && runwork dev`);
68
+ if (knownAgent.id === 'claude-desktop') {
69
+ console.log(` ${dim('2.')} ${agentCmd}`);
70
+ }
71
+ else {
72
+ console.log(` ${dim('2.')} ${cyan(agentCmd)} ${dim('(in a second terminal)')}`);
73
+ }
74
+ console.log('');
75
+ return;
76
+ }
77
+ }
52
78
  const { select, isCancel } = await import('@clack/prompts');
53
79
  console.log('');
54
80
  const result = await select({
@@ -64,7 +90,6 @@ export async function runAgentWizard(directory) {
64
90
  const agent = SUPPORTED_AGENTS.find(a => a.id === result);
65
91
  if (!agent)
66
92
  return;
67
- const slug = directory.split('/').pop() || directory;
68
93
  const agentCmd = agent.command.replace(/{dir}/g, slug);
69
94
  console.log('');
70
95
  console.log(bold('Get started:'));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { resolveWorkspace, resolveApp } from '../resolve.js';
3
+ const mockWorkspaces = [
4
+ { id: 'ws-1', name: 'Team Alpha' },
5
+ { id: 'ws-2', name: 'Team Beta' },
6
+ ];
7
+ const mockApps = [
8
+ { id: 'app-1', name: 'My App', slug: 'my-app', workspaceId: 'ws-1', workspaceName: 'Team Alpha', gitRemoteUrl: '' },
9
+ { id: 'app-2', name: 'Other App', slug: 'other-app', workspaceId: 'ws-2', workspaceName: 'Team Beta', gitRemoteUrl: '' },
10
+ ];
11
+ function createMockClient(workspaces, apps) {
12
+ return {
13
+ listWorkspaces: vi.fn().mockResolvedValue(workspaces),
14
+ listApps: vi.fn().mockResolvedValue(apps),
15
+ };
16
+ }
17
+ // Mock process.exit to throw instead of exiting
18
+ const mockExit = vi.spyOn(process, 'exit').mockImplementation(((code) => {
19
+ throw new Error(`process.exit(${code})`);
20
+ }));
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ vi.spyOn(console, 'error').mockImplementation(() => { });
24
+ });
25
+ describe('resolveWorkspace', () => {
26
+ it('resolves by exact ID', async () => {
27
+ const client = createMockClient(mockWorkspaces, []);
28
+ const ws = await resolveWorkspace(client, 'ws-1');
29
+ expect(ws).toEqual(mockWorkspaces[0]);
30
+ });
31
+ it('resolves by name (case-insensitive)', async () => {
32
+ const client = createMockClient(mockWorkspaces, []);
33
+ const ws = await resolveWorkspace(client, 'team beta');
34
+ expect(ws).toEqual(mockWorkspaces[1]);
35
+ });
36
+ it('prefers ID match over name match', async () => {
37
+ const workspaces = [
38
+ { id: 'ws-1', name: 'ws-2' }, // name matches a different workspace's ID
39
+ { id: 'ws-2', name: 'Other' },
40
+ ];
41
+ const client = createMockClient(workspaces, []);
42
+ const ws = await resolveWorkspace(client, 'ws-2');
43
+ expect(ws.id).toBe('ws-2');
44
+ });
45
+ it('exits when no workspaces found', async () => {
46
+ const client = createMockClient([], []);
47
+ await expect(resolveWorkspace(client, 'any')).rejects.toThrow('process.exit(1)');
48
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('No workspaces found'));
49
+ });
50
+ it('exits when workspace not found', async () => {
51
+ const client = createMockClient(mockWorkspaces, []);
52
+ await expect(resolveWorkspace(client, 'nonexistent')).rejects.toThrow('process.exit(1)');
53
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
54
+ });
55
+ it('exits on ambiguous name match', async () => {
56
+ const workspaces = [
57
+ { id: 'ws-1', name: 'Team' },
58
+ { id: 'ws-2', name: 'Team' },
59
+ ];
60
+ const client = createMockClient(workspaces, []);
61
+ await expect(resolveWorkspace(client, 'Team')).rejects.toThrow('process.exit(1)');
62
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Ambiguous'));
63
+ });
64
+ });
65
+ describe('resolveApp', () => {
66
+ it('resolves by exact ID', async () => {
67
+ const client = createMockClient([], mockApps);
68
+ const app = await resolveApp(client, 'app-1');
69
+ expect(app).toEqual(mockApps[0]);
70
+ });
71
+ it('resolves by name (case-insensitive)', async () => {
72
+ const client = createMockClient([], mockApps);
73
+ const app = await resolveApp(client, 'my app');
74
+ expect(app).toEqual(mockApps[0]);
75
+ });
76
+ it('exits when no apps found', async () => {
77
+ const client = createMockClient([], []);
78
+ await expect(resolveApp(client, 'any')).rejects.toThrow('process.exit(1)');
79
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('No apps found'));
80
+ });
81
+ it('exits when app not found', async () => {
82
+ const client = createMockClient([], mockApps);
83
+ await expect(resolveApp(client, 'nonexistent')).rejects.toThrow('process.exit(1)');
84
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
85
+ });
86
+ it('exits on ambiguous name match', async () => {
87
+ const apps = [
88
+ { id: 'app-1', name: 'Dup', slug: 'dup-1', workspaceId: 'ws-1', workspaceName: 'WS', gitRemoteUrl: '' },
89
+ { id: 'app-2', name: 'Dup', slug: 'dup-2', workspaceId: 'ws-1', workspaceName: 'WS', gitRemoteUrl: '' },
90
+ ];
91
+ const client = createMockClient([], apps);
92
+ await expect(resolveApp(client, 'Dup')).rejects.toThrow('process.exit(1)');
93
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Ambiguous'));
94
+ });
95
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import path from 'path';
3
+ // Mock better-sqlite3 for controlled tests
4
+ vi.mock('better-sqlite3', () => {
5
+ const MockDatabase = vi.fn();
6
+ return { default: MockDatabase };
7
+ });
8
+ import Database from 'better-sqlite3';
9
+ import { querySqlite } from '../sqlite.js';
10
+ const FIXTURE_DB = path.resolve(__dirname, '../../agents/__tests__/fixtures/codex/state.sqlite');
11
+ beforeEach(() => {
12
+ vi.restoreAllMocks();
13
+ });
14
+ describe('querySqlite', () => {
15
+ // ── Tests using the real fixture database ──────────────────────────
16
+ describe('with real fixture database', () => {
17
+ beforeEach(() => {
18
+ // Restore the real better-sqlite3 implementation for fixture tests
19
+ vi.mocked(Database).mockImplementation((...args) => {
20
+ const RealDatabase = vi.importActual('better-sqlite3');
21
+ // importActual returns a promise, but we need synchronous access;
22
+ // however vi.importActual in the module-scope mock is async.
23
+ // Instead, re-require the real module.
24
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
25
+ const Actual = require('better-sqlite3');
26
+ return new Actual(...args);
27
+ });
28
+ });
29
+ it('returns pipe-separated values for multi-column results', () => {
30
+ const result = querySqlite(FIXTURE_DB, "SELECT id, tokens_used FROM threads WHERE id = 'old-session'");
31
+ expect(result).toBe('old-session|50000');
32
+ });
33
+ it('returns newline-separated rows for multiple rows', () => {
34
+ const result = querySqlite(FIXTURE_DB, 'SELECT id, tokens_used FROM threads ORDER BY created_at');
35
+ const lines = result.split('\n');
36
+ expect(lines).toHaveLength(3);
37
+ expect(lines[0]).toContain('old-session');
38
+ expect(lines[1]).toContain('recent-session');
39
+ expect(lines[2]).toContain('newest-session');
40
+ });
41
+ });
42
+ // ── Tests using mocked database ────────────────────────────────────
43
+ describe('error handling', () => {
44
+ it('returns empty string when database does not exist', () => {
45
+ vi.mocked(Database).mockImplementation(() => {
46
+ throw new Error('SQLITE_CANTOPEN: unable to open database file');
47
+ });
48
+ const result = querySqlite('/nonexistent/path.db', 'SELECT 1');
49
+ expect(result).toBe('');
50
+ });
51
+ it('returns empty string when SQL is invalid', () => {
52
+ const mockPrepare = vi.fn().mockImplementation(() => {
53
+ throw new Error('SQLITE_ERROR: near "INVALID": syntax error');
54
+ });
55
+ const mockClose = vi.fn();
56
+ vi.mocked(Database).mockImplementation(() => ({ prepare: mockPrepare, close: mockClose }));
57
+ const result = querySqlite('/some/db.sqlite', 'INVALID SQL QUERY');
58
+ expect(result).toBe('');
59
+ });
60
+ it('returns empty string when database is corrupted', () => {
61
+ vi.mocked(Database).mockImplementation(() => {
62
+ throw new Error('SQLITE_CORRUPT: database disk image is malformed');
63
+ });
64
+ const result = querySqlite('/corrupted/db.sqlite', 'SELECT 1');
65
+ expect(result).toBe('');
66
+ });
67
+ it('closes database even on query error', () => {
68
+ const mockClose = vi.fn();
69
+ const mockPrepare = vi.fn().mockImplementation(() => {
70
+ throw new Error('query failed');
71
+ });
72
+ vi.mocked(Database).mockImplementation(() => ({ prepare: mockPrepare, close: mockClose }));
73
+ querySqlite('/some/db.sqlite', 'SELECT 1');
74
+ expect(mockClose).toHaveBeenCalledTimes(1);
75
+ });
76
+ });
77
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Centralized agent teaching content.
3
+ * All guidance strings and builder functions live here so when Runwork
4
+ * features change, one file updates.
5
+ */
6
+ export interface AgentGuide {
7
+ context: string;
8
+ structure?: Record<string, string>;
9
+ nextSteps: string[];
10
+ tips?: string[];
11
+ learnMore?: string;
12
+ }
13
+ export interface AgentError {
14
+ message: string;
15
+ diagnosis: string;
16
+ suggestions: string[];
17
+ }
18
+ export interface AgentResponse<T> {
19
+ success: boolean;
20
+ command: string;
21
+ result: T;
22
+ guide?: AgentGuide;
23
+ error?: AgentError;
24
+ }
25
+ /** Map of key template files and what they're for. */
26
+ export declare const APP_STRUCTURE: Record<string, string>;
27
+ export declare function buildInitGuide(appName: string, dir: string): AgentGuide;
28
+ export declare function buildCloneGuide(appName: string, dir: string): AgentGuide;
29
+ export declare function buildDevSessionGuide(): AgentGuide;
30
+ export declare function buildDeployGuide(): AgentGuide;
31
+ export declare function buildInfoGuide(): AgentGuide;
32
+ export declare function buildErrorResponse(command: string, message: string, diagnosis: string, suggestions: string[]): AgentResponse<null>;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Centralized agent teaching content.
3
+ * All guidance strings and builder functions live here so when Runwork
4
+ * features change, one file updates.
5
+ */
6
+ /** Map of key template files and what they're for. */
7
+ export const APP_STRUCTURE = {
8
+ 'worker/entities.ts': 'Define persistent data models. Extend Entity<State> for storage with CRUD.',
9
+ 'worker/schedules.ts': 'Add background cron jobs. Standard 5-field cron syntax.',
10
+ 'worker/workflows.ts': 'Multi-step durable processes. Use ctx.step.do() for retryable steps.',
11
+ 'worker/routes.ts': 'Internal API endpoints the frontend calls. CRUD operations, search, filters.',
12
+ 'worker/endpoints.ts': 'Public APIs for external access. Webhooks, third-party integrations.',
13
+ 'worker/agents.ts': 'AI agents -- conversational (chat UI) or task (headless automation).',
14
+ 'worker/integration-requirements.ts': 'Declare third-party service dependencies (Slack, GitHub, etc.).',
15
+ 'worker/components.ts': 'Reusable UI components other workspace apps can embed.',
16
+ 'shared/types.ts': 'TypeScript interfaces shared between frontend and backend.',
17
+ 'src/pages/': 'Frontend React pages. Each file becomes a route.',
18
+ 'blueprint.json': 'App feature registry. Update after adding entities, workflows, agents, etc.',
19
+ 'CLAUDE.md': 'Complete framework documentation. Read this before editing anything.',
20
+ };
21
+ const COMMON_TIPS = [
22
+ 'Read CLAUDE.md in the app directory first -- it has complete framework documentation with code examples.',
23
+ 'You do NOT need git commands. runwork dev handles file syncing automatically.',
24
+ 'Do NOT install external AI SDKs (openai, @anthropic-ai/sdk). Use @runworkai/framework/ai instead.',
25
+ 'Every workflow MUST have a trigger (API endpoint, scheduled job, or UI button). Workflows without triggers are dead code.',
26
+ 'Every conversational agent MUST have a frontend page to access it.',
27
+ 'After adding entities, workflows, or agents, update blueprint.json with the new metadata.',
28
+ 'Never guess integration IDs. Run: runwork integrations search <query>',
29
+ ];
30
+ export function buildInitGuide(appName, dir) {
31
+ return {
32
+ context: 'Created a Runwork app -- a full-stack Cloudflare Workers application with built-in support for persistent data (entities), background jobs (schedules), multi-step durable processes (workflows), AI agents, public APIs, and third-party integrations. The app runs in a sandbox during development and deploys to Cloudflare Workers for production.',
33
+ structure: APP_STRUCTURE,
34
+ nextSteps: [
35
+ `cd ${dir}`,
36
+ 'runwork dev # start development server -- watches for file changes and syncs automatically',
37
+ 'Edit files for your needs (see structure above)',
38
+ 'runwork deploy # deploy to production when ready',
39
+ ],
40
+ tips: COMMON_TIPS,
41
+ learnMore: 'Read CLAUDE.md in the app directory for complete framework documentation with code examples.',
42
+ };
43
+ }
44
+ export function buildCloneGuide(appName, dir) {
45
+ return {
46
+ context: 'Cloned an existing Runwork app. This app may already have entities, workflows, agents, and other features configured. Check the existing files before making changes.',
47
+ structure: APP_STRUCTURE,
48
+ nextSteps: [
49
+ `cd ${dir}`,
50
+ 'runwork dev # start development server',
51
+ 'Review existing files to understand what is already built',
52
+ 'Edit files for your needs',
53
+ 'runwork deploy # deploy to production when ready',
54
+ ],
55
+ tips: COMMON_TIPS,
56
+ learnMore: 'Read CLAUDE.md in the app directory for complete framework documentation.',
57
+ };
58
+ }
59
+ export function buildDevSessionGuide() {
60
+ return {
61
+ context: 'Development server is running. Files are watched and synced to a sandbox automatically. Edit your code and changes will appear at the preview URL.',
62
+ nextSteps: [
63
+ 'Edit files in the app directory -- changes sync automatically',
64
+ 'Open the preview URL to see your app',
65
+ 'Run runwork deploy in another terminal when ready for production',
66
+ ],
67
+ tips: [
68
+ 'You do NOT need git commands. The dev server handles all file syncing.',
69
+ 'If the preview seems stuck, try runwork dev again to restart the session.',
70
+ ],
71
+ };
72
+ }
73
+ export function buildDeployGuide() {
74
+ return {
75
+ context: 'App deployed to production on Cloudflare Workers. The production URL is live and serving traffic. Registered resources (entities, schedules, workflows, agents) are active.',
76
+ nextSteps: [
77
+ 'Verify the app at the production URL',
78
+ 'Run runwork info to see all registered resources and their status',
79
+ 'Share the production URL with users or integrate it into other systems',
80
+ ],
81
+ };
82
+ }
83
+ export function buildInfoGuide() {
84
+ return {
85
+ context: 'This shows the current state of the app: what is deployed, what integrations are connected, and what resources (entities, workflows, agents, etc.) are registered.',
86
+ nextSteps: [
87
+ 'runwork dev # start development server if not already running',
88
+ 'runwork deploy # deploy to production when ready',
89
+ ],
90
+ tips: [
91
+ 'Entities listed here are the data models available via Entity CRUD methods.',
92
+ 'Workflows listed here can be triggered via their registered endpoints or schedules.',
93
+ 'Agents listed here need frontend pages (conversational) or triggers (task) to be accessible.',
94
+ 'Integrations with connected status are ready to use. Others need setup in workspace settings.',
95
+ 'If preview is not active, run runwork dev to start a development session.',
96
+ ],
97
+ };
98
+ }
99
+ export function buildErrorResponse(command, message, diagnosis, suggestions) {
100
+ return {
101
+ success: false,
102
+ command,
103
+ result: null,
104
+ error: { message, diagnosis, suggestions },
105
+ };
106
+ }
package/dist/utils/fs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readdirSync, rmSync } from 'fs';
2
- import { join, relative } from 'path';
2
+ import { join, relative, sep } from 'path';
3
3
  /**
4
4
  * Remove .git directories nested at depth >= 2 under the given root.
5
5
  * Replaces `find <dir> -mindepth 2 -name .git -type d`.
@@ -14,7 +14,7 @@ export function removeNestedGitDirs(rootDir) {
14
14
  : entry.path;
15
15
  const fullPath = join(parentPath, entry.name);
16
16
  const rel = relative(rootDir, fullPath);
17
- const depth = rel.split('/').length;
17
+ const depth = rel.split(sep).length;
18
18
  // Only remove .git dirs at depth >= 2 (not the root .git)
19
19
  if (depth >= 2) {
20
20
  rmSync(fullPath, { recursive: true, force: true });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * JSON help output for AI agents.
3
+ *
4
+ * When --json is active (explicitly or via non-TTY), `runwork --help` and
5
+ * `runwork <command> --help` output a machine-readable command tree instead
6
+ * of Commander's human-readable text.
7
+ *
8
+ * This lets an agent discover the full CLI surface in a single call.
9
+ */
10
+ import type { Command } from 'commander';
11
+ interface JsonOption {
12
+ flags: string;
13
+ description: string;
14
+ required: boolean;
15
+ optional: boolean;
16
+ variadic: boolean;
17
+ defaultValue?: unknown;
18
+ choices?: string[];
19
+ envVar?: string;
20
+ }
21
+ interface JsonArgument {
22
+ name: string;
23
+ description: string;
24
+ required: boolean;
25
+ variadic: boolean;
26
+ defaultValue?: unknown;
27
+ choices?: string[];
28
+ }
29
+ interface JsonCommand {
30
+ name: string;
31
+ description: string;
32
+ aliases: string[];
33
+ usage: string;
34
+ options: JsonOption[];
35
+ arguments: JsonArgument[];
36
+ subcommands: JsonCommand[];
37
+ }
38
+ interface JsonHelpOutput {
39
+ version: string;
40
+ name: string;
41
+ description: string;
42
+ usage: string;
43
+ globalOptions: JsonOption[];
44
+ commands: JsonCommand[];
45
+ }
46
+ /**
47
+ * Serialize the full command tree to a JSON-friendly structure.
48
+ */
49
+ export declare function buildHelpJson(program: Command): JsonHelpOutput;
50
+ /**
51
+ * Serialize a single command (for `runwork <cmd> --help --json`).
52
+ */
53
+ export declare function buildCommandHelpJson(cmd: Command, version: string): JsonCommand & {
54
+ version: string;
55
+ };
56
+ export {};
@@ -0,0 +1,85 @@
1
+ /**
2
+ * JSON help output for AI agents.
3
+ *
4
+ * When --json is active (explicitly or via non-TTY), `runwork --help` and
5
+ * `runwork <command> --help` output a machine-readable command tree instead
6
+ * of Commander's human-readable text.
7
+ *
8
+ * This lets an agent discover the full CLI surface in a single call.
9
+ */
10
+ function serializeOption(opt) {
11
+ const result = {
12
+ flags: opt.flags,
13
+ description: opt.description,
14
+ required: opt.required,
15
+ optional: opt.optional,
16
+ variadic: opt.variadic,
17
+ };
18
+ if (opt.defaultValue !== undefined)
19
+ result.defaultValue = opt.defaultValue;
20
+ if (opt.argChoices)
21
+ result.choices = opt.argChoices;
22
+ if (opt.envVar)
23
+ result.envVar = opt.envVar;
24
+ return result;
25
+ }
26
+ function serializeArgument(arg) {
27
+ const result = {
28
+ name: arg.name(),
29
+ description: arg.description,
30
+ required: arg.required,
31
+ variadic: arg.variadic,
32
+ };
33
+ if (arg.defaultValue !== undefined)
34
+ result.defaultValue = arg.defaultValue;
35
+ if (arg.argChoices)
36
+ result.choices = arg.argChoices;
37
+ return result;
38
+ }
39
+ function serializeCommand(cmd) {
40
+ const subcommands = cmd.commands
41
+ .filter((c) => !c._hidden)
42
+ .map(serializeCommand);
43
+ // Get command-level options (exclude inherited global options)
44
+ const options = cmd.options
45
+ .filter((o) => !o.hidden)
46
+ .map(serializeOption);
47
+ const registeredArguments = cmd.registeredArguments ?? [];
48
+ return {
49
+ name: cmd.name(),
50
+ description: cmd.description(),
51
+ aliases: cmd.aliases(),
52
+ usage: cmd.usage(),
53
+ options,
54
+ arguments: registeredArguments.map(serializeArgument),
55
+ subcommands,
56
+ };
57
+ }
58
+ /**
59
+ * Serialize the full command tree to a JSON-friendly structure.
60
+ */
61
+ export function buildHelpJson(program) {
62
+ const globalOptions = program.options
63
+ .filter((o) => !o.hidden)
64
+ .map(serializeOption);
65
+ const commands = program.commands
66
+ .filter((c) => !c._hidden)
67
+ .map(serializeCommand);
68
+ return {
69
+ version: program.version() ?? 'unknown',
70
+ name: program.name(),
71
+ description: program.description(),
72
+ usage: program.usage(),
73
+ globalOptions,
74
+ commands,
75
+ };
76
+ }
77
+ /**
78
+ * Serialize a single command (for `runwork <cmd> --help --json`).
79
+ */
80
+ export function buildCommandHelpJson(cmd, version) {
81
+ return {
82
+ version,
83
+ ...serializeCommand(cmd),
84
+ };
85
+ }
@@ -0,0 +1,12 @@
1
+ import type { ApiClient } from '../api/client.js';
2
+ import type { AppInfo, WorkspaceInfo } from '../types.js';
3
+ /**
4
+ * Resolve a workspace by name or ID from the user's accessible workspaces.
5
+ * Tries exact ID match first, then case-insensitive name match.
6
+ */
7
+ export declare function resolveWorkspace(client: ApiClient, nameOrId: string): Promise<WorkspaceInfo>;
8
+ /**
9
+ * Resolve an app by name or ID from the user's accessible apps.
10
+ * Tries exact ID match first, then case-insensitive name match.
11
+ */
12
+ export declare function resolveApp(client: ApiClient, nameOrId: string): Promise<AppInfo>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Resolve a workspace by name or ID from the user's accessible workspaces.
3
+ * Tries exact ID match first, then case-insensitive name match.
4
+ */
5
+ export async function resolveWorkspace(client, nameOrId) {
6
+ const workspaces = await client.listWorkspaces();
7
+ if (workspaces.length === 0) {
8
+ console.error('No workspaces found. Create one at runwork.ai first.');
9
+ process.exit(1);
10
+ }
11
+ // Exact ID match
12
+ const byId = workspaces.find(w => w.id === nameOrId);
13
+ if (byId)
14
+ return byId;
15
+ // Case-insensitive name match
16
+ const lower = nameOrId.toLowerCase();
17
+ const byName = workspaces.filter(w => w.name.toLowerCase() === lower);
18
+ if (byName.length === 1)
19
+ return byName[0];
20
+ if (byName.length > 1) {
21
+ console.error(`Ambiguous workspace name "${nameOrId}". Matches: ${byName.map(w => `${w.name} (${w.id})`).join(', ')}`);
22
+ process.exit(1);
23
+ }
24
+ console.error(`Workspace "${nameOrId}" not found. Available: ${workspaces.map(w => w.name).join(', ')}`);
25
+ process.exit(1);
26
+ }
27
+ /**
28
+ * Resolve an app by name or ID from the user's accessible apps.
29
+ * Tries exact ID match first, then case-insensitive name match.
30
+ */
31
+ export async function resolveApp(client, nameOrId) {
32
+ const apps = await client.listApps();
33
+ if (apps.length === 0) {
34
+ console.error('No apps found.');
35
+ process.exit(1);
36
+ }
37
+ // Exact ID match
38
+ const byId = apps.find(a => a.id === nameOrId);
39
+ if (byId)
40
+ return byId;
41
+ // Case-insensitive name match
42
+ const lower = nameOrId.toLowerCase();
43
+ const byName = apps.filter(a => a.name.toLowerCase() === lower);
44
+ if (byName.length === 1)
45
+ return byName[0];
46
+ if (byName.length > 1) {
47
+ console.error(`Ambiguous app name "${nameOrId}". Matches: ${byName.map(a => `${a.name} (${a.id})`).join(', ')}`);
48
+ process.exit(1);
49
+ }
50
+ console.error(`App "${nameOrId}" not found. Available: ${apps.map(a => a.name).join(', ')}`);
51
+ process.exit(1);
52
+ }
@@ -0,0 +1,3 @@
1
+ import type { SetupState } from '../types.js';
2
+ /** Load setup state, checking project-level then user-level. */
3
+ export declare function loadSetupState(): SetupState | null;
@@ -0,0 +1,19 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ /** Load setup state, checking project-level then user-level. */
5
+ export function loadSetupState() {
6
+ const projectPath = join(process.cwd(), '.runwork', 'setup.json');
7
+ const userPath = join(homedir(), '.runwork', 'setup.json');
8
+ for (const p of [projectPath, userPath]) {
9
+ if (existsSync(p)) {
10
+ try {
11
+ return JSON.parse(readFileSync(p, 'utf-8'));
12
+ }
13
+ catch {
14
+ continue;
15
+ }
16
+ }
17
+ }
18
+ return null;
19
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Query a SQLite database using better-sqlite3.
3
+ * Cross-platform, no CLI dependency.
4
+ * Opens database in readonly mode with a 5-second busy timeout.
5
+ * Returns empty string on any error (missing DB, locked, corrupted).
6
+ */
7
+ export declare function querySqlite(dbPath: string, sql: string): string;
@@ -0,0 +1,24 @@
1
+ import Database from 'better-sqlite3';
2
+ /**
3
+ * Query a SQLite database using better-sqlite3.
4
+ * Cross-platform, no CLI dependency.
5
+ * Opens database in readonly mode with a 5-second busy timeout.
6
+ * Returns empty string on any error (missing DB, locked, corrupted).
7
+ */
8
+ export function querySqlite(dbPath, sql) {
9
+ let db = null;
10
+ try {
11
+ db = new Database(dbPath, { readonly: true, timeout: 5000 });
12
+ const rows = db.prepare(sql).all();
13
+ return rows.map(row => Object.values(row).join('|')).join('\n');
14
+ }
15
+ catch {
16
+ return '';
17
+ }
18
+ finally {
19
+ try {
20
+ db?.close();
21
+ }
22
+ catch { /* ignore close errors */ }
23
+ }
24
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Cross-platform binary detection.
3
+ * Uses `which` on macOS/Linux, `where.exe` on Windows.
4
+ * Returns the resolved path on success, null if not found.
5
+ */
6
+ export declare function whichBinary(name: string): string | null;