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
@@ -20,6 +20,106 @@ export async function performLogin(baseUrl) {
20
20
  console.log(`If browser didn't open, visit: ${loginUrl}`);
21
21
  console.log('');
22
22
  console.log('Waiting for authentication...');
23
+ return pollAndSave(client, sessionId, url);
24
+ }
25
+ /**
26
+ * Initiate a login session without opening the browser.
27
+ * Prints the login URL to stdout and polls for completion.
28
+ * If printOnly is true, prints the URL and returns null (exits without polling).
29
+ */
30
+ export async function performLoginNoOpen(baseUrl, printOnly) {
31
+ const url = baseUrl || DEFAULT_BASE_URL;
32
+ const client = new ApiClient({ apiKey: '', email: '', baseUrl: url });
33
+ const { loginUrl } = await client.initiateLogin();
34
+ // Print the URL for the agent/human to use
35
+ console.log(loginUrl);
36
+ if (printOnly) {
37
+ return null;
38
+ }
39
+ console.error('Waiting for authentication...');
40
+ return pollAndSave(client, extractSessionId(loginUrl), url);
41
+ }
42
+ /**
43
+ * Resume a previously initiated login session by providing the login URL.
44
+ * Extracts the session ID from the URL and polls until authenticated.
45
+ */
46
+ export async function performLoginFromUrl(loginUrl) {
47
+ const sessionId = extractSessionId(loginUrl);
48
+ const baseUrl = extractBaseUrl(loginUrl);
49
+ const client = new ApiClient({ apiKey: '', email: '', baseUrl });
50
+ console.log('Waiting for authentication...');
51
+ return pollAndSave(client, sessionId, baseUrl);
52
+ }
53
+ /**
54
+ * Authenticate directly with an API key (no browser flow).
55
+ * Validates the key by calling the API, then saves credentials.
56
+ */
57
+ export async function performLoginWithApiKey(apiKey, baseUrl) {
58
+ const url = baseUrl || DEFAULT_BASE_URL;
59
+ const client = new ApiClient({ apiKey, email: '', baseUrl: url });
60
+ // Validate the key by fetching workspaces
61
+ try {
62
+ const workspaces = await client.listWorkspaces();
63
+ const defaultWs = workspaces[0];
64
+ const creds = {
65
+ apiKey,
66
+ email: 'api-key-auth',
67
+ baseUrl: url,
68
+ defaultWorkspaceId: defaultWs?.id,
69
+ defaultWorkspaceName: defaultWs?.name,
70
+ };
71
+ saveCredentials(creds);
72
+ await configureGitCredentials(url);
73
+ console.log('Logged in with API key.');
74
+ if (defaultWs) {
75
+ console.log(`Default workspace: ${defaultWs.name}`);
76
+ }
77
+ return creds;
78
+ }
79
+ catch (err) {
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ console.error(`Invalid API key: ${message}`);
82
+ process.exit(1);
83
+ }
84
+ }
85
+ /**
86
+ * Extract session ID from a login URL.
87
+ * Supports both query param (?session=xxx) and path segment formats.
88
+ */
89
+ function extractSessionId(loginUrl) {
90
+ try {
91
+ const url = new URL(loginUrl);
92
+ const session = url.searchParams.get('session');
93
+ if (session)
94
+ return session;
95
+ // Try path-based: /cli-auth/SESSION_ID
96
+ const pathParts = url.pathname.split('/');
97
+ const authIdx = pathParts.indexOf('cli-auth');
98
+ if (authIdx >= 0 && pathParts[authIdx + 1]) {
99
+ return pathParts[authIdx + 1];
100
+ }
101
+ }
102
+ catch {
103
+ // Not a valid URL
104
+ }
105
+ console.error(`Could not extract session ID from URL: ${loginUrl}`);
106
+ process.exit(1);
107
+ }
108
+ /**
109
+ * Extract the base URL (origin) from a login URL.
110
+ */
111
+ function extractBaseUrl(loginUrl) {
112
+ try {
113
+ return new URL(loginUrl).origin;
114
+ }
115
+ catch {
116
+ return DEFAULT_BASE_URL;
117
+ }
118
+ }
119
+ /**
120
+ * Poll for login completion and save credentials.
121
+ */
122
+ async function pollAndSave(client, sessionId, baseUrl) {
23
123
  const maxAttempts = 60;
24
124
  const pollInterval = 5000;
25
125
  for (let i = 0; i < maxAttempts; i++) {
@@ -27,7 +127,7 @@ export async function performLogin(baseUrl) {
27
127
  const result = await client.pollLogin(sessionId);
28
128
  if (result) {
29
129
  saveCredentials(result);
30
- await configureGitCredentials(result.baseUrl || url);
130
+ await configureGitCredentials(result.baseUrl || baseUrl);
31
131
  console.log(`Logged in as ${result.email}`);
32
132
  return result;
33
133
  }
@@ -1,5 +1,13 @@
1
1
  import { Command } from 'commander';
2
2
  import { ApiClient } from '../api/client.js';
3
3
  import type { AppInfo } from '../types.js';
4
- export declare function execClone(client: ApiClient, app: AppInfo, directory?: string): Promise<string>;
4
+ export interface CloneResult {
5
+ appId: string;
6
+ appName: string;
7
+ slug: string;
8
+ directory: string;
9
+ workspaceId: string;
10
+ workspaceName: string;
11
+ }
12
+ export declare function execClone(client: ApiClient, app: AppInfo, directory?: string): Promise<CloneResult>;
5
13
  export declare const cloneCommand: Command;
@@ -5,10 +5,13 @@ import { join, resolve } from 'path';
5
5
  import { requireAuth } from '../auth/store.js';
6
6
  import { ApiClient } from '../api/client.js';
7
7
  import { promptSelect } from '../utils/prompt.js';
8
+ import { resolveApp } from '../utils/resolve.js';
8
9
  import { generateManifest, saveManifest } from '../template/manifest.js';
9
10
  import { extractZip } from '../utils/zip.js';
10
11
  import { removeNestedGitDirs } from '../utils/fs.js';
11
12
  import { runAgentWizard } from '../ui/banner.js';
13
+ import { shouldOutputJson, jsonOut } from '../utils/output.js';
14
+ import { buildCloneGuide, buildErrorResponse } from '../utils/agent-guidance.js';
12
15
  export async function execClone(client, app, directory) {
13
16
  const slug = app.slug || app.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
14
17
  const dir = directory || slug;
@@ -39,7 +42,7 @@ export async function execClone(client, app, directory) {
39
42
  execFileSync('git', ['remote', 'add', 'runwork', remoteUrl], { cwd: dir, stdio: 'pipe' });
40
43
  }
41
44
  try {
42
- execFileSync('git', ['fetch', 'runwork', 'main'], { cwd: dir, stdio: 'inherit' });
45
+ execFileSync('git', ['fetch', 'runwork', 'main'], { cwd: dir, stdio: 'pipe' });
43
46
  // Reset to remote HEAD - overlays git-tracked files on top of template
44
47
  execFileSync('git', ['reset', '--mixed', 'runwork/main'], { cwd: dir, stdio: 'pipe' });
45
48
  // Checkout git-tracked files (overwrites template files where they differ)
@@ -66,33 +69,58 @@ export async function execClone(client, app, directory) {
66
69
  catch {
67
70
  // App may be new with no registries yet
68
71
  }
69
- console.log(`\nApp "${app.name}" cloned to ${dir}/`);
70
- console.log(`Remote: ${client.getGitRemoteUrl(app.workspaceId, app.id)}`);
71
- return resolve(dir);
72
+ return {
73
+ appId: app.id,
74
+ appName: app.name,
75
+ slug,
76
+ directory: resolve(dir),
77
+ workspaceId: app.workspaceId,
78
+ workspaceName: app.workspaceName,
79
+ };
72
80
  }
73
81
  export const cloneCommand = new Command('clone')
74
82
  .description('Clone a Runwork app to local development')
75
83
  .argument('[appId]', 'App ID to clone (interactive if omitted)')
76
84
  .argument('[directory]', 'Target directory')
77
- .action(async (appId, directory) => {
85
+ .option('--app <name-or-id>', 'App name or ID (skips interactive selection)')
86
+ .action(async (appId, directory, options) => {
78
87
  const creds = requireAuth();
79
88
  const client = new ApiClient(creds);
89
+ const useJson = shouldOutputJson(undefined);
80
90
  let app;
81
- if (appId) {
82
- app = await client.getApp(appId);
91
+ const appRef = options?.app || appId;
92
+ if (appRef) {
93
+ app = await resolveApp(client, appRef);
83
94
  }
84
95
  else {
85
96
  const apps = await client.listApps();
86
97
  if (apps.length === 0) {
98
+ if (useJson) {
99
+ jsonOut(buildErrorResponse('clone', 'No apps found', 'No apps are available to clone.', ['Create an app first with: runwork init my-app']));
100
+ return;
101
+ }
87
102
  console.error('No apps found.');
88
103
  process.exit(1);
89
104
  }
90
105
  const choice = await promptSelect('Select app to clone:', apps.map(a => ({ label: `${a.name} (${a.workspaceName})`, value: a })));
91
106
  app = choice.value;
92
107
  }
93
- console.log(`Cloning "${app.name}"...`);
94
- const dir = await execClone(client, app, directory);
95
- const slug = dir.split('/').pop() || dir;
96
- await runAgentWizard(dir);
97
- console.log(`Next: cd ${slug} && runwork dev`);
108
+ if (!useJson) {
109
+ console.log(`Cloning "${app.name}"...`);
110
+ }
111
+ const cloneResult = await execClone(client, app, directory);
112
+ if (useJson) {
113
+ const response = {
114
+ success: true,
115
+ command: 'clone',
116
+ result: cloneResult,
117
+ guide: buildCloneGuide(cloneResult.appName, cloneResult.slug),
118
+ };
119
+ jsonOut(response);
120
+ return;
121
+ }
122
+ console.log(`\nApp "${cloneResult.appName}" cloned to ${cloneResult.directory}/`);
123
+ console.log(`Remote: ${client.getGitRemoteUrl(cloneResult.workspaceId, cloneResult.appId)}`);
124
+ await runAgentWizard(cloneResult.directory);
125
+ console.log(`Next: cd ${cloneResult.slug} && runwork dev`);
98
126
  });
@@ -4,13 +4,14 @@ import { readFileSync, existsSync } from 'fs';
4
4
  import { requireAuth } from '../auth/store.js';
5
5
  import { ApiClient } from '../api/client.js';
6
6
  import { shouldOutputJson, jsonOut } from '../utils/output.js';
7
+ import { buildDeployGuide, buildErrorResponse } from '../utils/agent-guidance.js';
7
8
  export const deployCommand = new Command('deploy')
8
9
  .description('Deploy the current app to production')
9
10
  .action(async (_opts, command) => {
10
11
  const useJson = shouldOutputJson(command.optsWithGlobals().json);
11
12
  if (!existsSync('.runwork.json')) {
12
13
  if (useJson) {
13
- process.stderr.write(JSON.stringify({ error: 'No .runwork.json found. Run `runwork init` first.' }) + '\n');
14
+ jsonOut(buildErrorResponse('deploy', 'No .runwork.json found', 'This directory is not a Runwork app.', ['Run runwork init to create an app first', 'Or cd into an existing app directory']));
14
15
  process.exit(1);
15
16
  }
16
17
  console.error('No .runwork.json found. Run `runwork init` first.');
@@ -42,7 +43,7 @@ export const deployCommand = new Command('deploy')
42
43
  }
43
44
  else {
44
45
  if (useJson) {
45
- process.stderr.write(JSON.stringify({ error: 'No commits to deploy. Commit your changes first.' }) + '\n');
46
+ jsonOut(buildErrorResponse('deploy', 'Sync failed before deployment', 'Files could not be synced to the remote.', ['Run runwork dev first to ensure files sync correctly', 'Check your network connection', 'Retry runwork deploy']));
46
47
  process.exit(1);
47
48
  }
48
49
  console.error('No commits to deploy. Commit your changes first.');
@@ -51,9 +52,31 @@ export const deployCommand = new Command('deploy')
51
52
  // Trigger deploy
52
53
  if (!useJson)
53
54
  console.log('Deploying...');
54
- const result = await client.triggerDeploy(config.appId);
55
+ let result;
56
+ try {
57
+ result = await client.triggerDeploy(config.appId);
58
+ }
59
+ catch (err) {
60
+ const message = err instanceof Error ? err.message : String(err);
61
+ if (useJson) {
62
+ jsonOut(buildErrorResponse('deploy', message, 'The deployment API returned an error.', ['Check for TypeScript errors in your code', 'Run runwork dev to test in the sandbox first', 'Retry runwork deploy']));
63
+ process.exit(1);
64
+ }
65
+ console.error(`Deploy failed: ${message}`);
66
+ process.exit(1);
67
+ }
55
68
  if (useJson) {
56
- jsonOut({ deployed: true, url: result.deploymentUrl });
69
+ const response = {
70
+ success: true,
71
+ command: 'deploy',
72
+ result: {
73
+ deployed: true,
74
+ url: result.deploymentUrl,
75
+ appName: config.appName,
76
+ },
77
+ guide: buildDeployGuide(),
78
+ };
79
+ jsonOut(response);
57
80
  return;
58
81
  }
59
82
  console.log(`Deployed: ${result.deploymentUrl}`);
@@ -16,6 +16,7 @@ import { createStatusLine } from '../ui/status-line.js';
16
16
  import { createKeyboardListener } from '../ui/keyboard.js';
17
17
  import { bold, dim, green, yellow, cyan } from '../ui/colors.js';
18
18
  import { shouldOutputJson, jsonLine } from '../utils/output.js';
19
+ import { buildDevSessionGuide } from '../utils/agent-guidance.js';
19
20
  async function populateSkill(projectDir, client, appId) {
20
21
  try {
21
22
  const { skill } = await client.getAppSkill(appId);
@@ -110,6 +111,39 @@ export async function execDev(options) {
110
111
  const syncResult = syncWithRemote(cwd);
111
112
  if (useJson) {
112
113
  jsonLine({ event: 'startup', phase: 'sync', status: syncResult.status, pushed: syncResult.pushed, timestamp: ts() });
114
+ if (syncResult.status === 'sync-failed') {
115
+ if (syncResult.error === 'stash-conflict') {
116
+ jsonLine({
117
+ event: 'error',
118
+ phase: 'sync',
119
+ timestamp: ts(),
120
+ error: {
121
+ message: 'Merge conflicts detected after stash pop',
122
+ diagnosis: 'Local changes could not be automatically merged with the remote state. The stash pop resulted in conflicts that require manual resolution.',
123
+ suggestions: [
124
+ 'Resolve the merge conflicts in the affected files',
125
+ 'Run git add on resolved files, then git commit',
126
+ 'Run runwork dev again after resolving conflicts',
127
+ ],
128
+ },
129
+ });
130
+ process.exit(1);
131
+ }
132
+ jsonLine({
133
+ event: 'error',
134
+ phase: 'sync',
135
+ timestamp: ts(),
136
+ error: {
137
+ message: syncResult.error ?? 'File sync failed',
138
+ diagnosis: 'Could not sync files to the sandbox. This usually means the sandbox session expired or the network connection was interrupted.',
139
+ suggestions: [
140
+ 'Run runwork dev again to start a fresh session',
141
+ 'Check your network connection',
142
+ 'Run runwork doctor to diagnose environment issues',
143
+ ],
144
+ },
145
+ });
146
+ }
113
147
  }
114
148
  else {
115
149
  switch (syncResult.status) {
@@ -139,7 +173,7 @@ export async function execDev(options) {
139
173
  }
140
174
  // Emit session_started (JSON) or show banner (human)
141
175
  if (useJson) {
142
- jsonLine({ event: 'session_started', previewUrl: session.previewUrl, appName: config.appName, timestamp: ts() });
176
+ jsonLine({ event: 'session_started', previewUrl: session.previewUrl, appName: config.appName, timestamp: ts(), guide: buildDevSessionGuide() });
143
177
  }
144
178
  else {
145
179
  console.log(getDevBanner({ appName: config.appName, previewUrl: session.previewUrl, workspaceName: config.workspaceName }));
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const doctorCommand: Command;
@@ -0,0 +1,82 @@
1
+ import { Command } from 'commander';
2
+ import { shouldOutputJson, jsonOut } from '../utils/output.js';
3
+ import { bold, dim, green, yellow, cyan } from '../ui/colors.js';
4
+ import { runAllChecks } from '../health/runner.js';
5
+ const STATUS_LABELS = {
6
+ pass: green('[pass]'),
7
+ warn: yellow('[warn]'),
8
+ fail: bold('[FAIL]'),
9
+ skip: dim('[skip]'),
10
+ };
11
+ const CHECK_LABELS = {
12
+ 'system-deps': 'System dependencies',
13
+ 'cli-version': 'CLI version',
14
+ 'auth': 'Authentication',
15
+ 'network': 'Network',
16
+ 'project-config': 'Project config',
17
+ 'app-exists': 'App exists',
18
+ 'git-remote': 'Git remote',
19
+ 'agent-setup': 'Agent setup',
20
+ };
21
+ function printHumanReport(report) {
22
+ console.log('');
23
+ console.log(bold('runwork doctor'));
24
+ console.log('');
25
+ for (const check of report.checks) {
26
+ const status = STATUS_LABELS[check.status] || check.status;
27
+ const label = (CHECK_LABELS[check.name] || check.name).padEnd(22);
28
+ console.log(` ${status} ${label} ${dim(check.message)}`);
29
+ if (check.details && check.details.length > 1) {
30
+ for (const detail of check.details) {
31
+ console.log(` ${dim('- ' + detail)}`);
32
+ }
33
+ }
34
+ }
35
+ // Summary
36
+ const failures = report.checks.filter(c => c.status === 'fail');
37
+ const warnings = report.checks.filter(c => c.status === 'warn');
38
+ console.log('');
39
+ if (failures.length > 0) {
40
+ console.log(` ${failures.length} issue${failures.length !== 1 ? 's' : ''} found:`);
41
+ for (const f of failures) {
42
+ const label = CHECK_LABELS[f.name] || f.name;
43
+ if (f.fix) {
44
+ console.log(` - ${label}: ${cyan(f.fix)}`);
45
+ }
46
+ else {
47
+ console.log(` - ${label}: ${f.message}`);
48
+ }
49
+ }
50
+ }
51
+ if (warnings.length > 0 && failures.length === 0) {
52
+ console.log(` ${warnings.length} warning${warnings.length !== 1 ? 's' : ''}:`);
53
+ for (const w of warnings) {
54
+ const label = CHECK_LABELS[w.name] || w.name;
55
+ if (w.fix) {
56
+ console.log(` - ${label}: ${cyan(w.fix)}`);
57
+ }
58
+ else {
59
+ console.log(` - ${label}: ${w.message}`);
60
+ }
61
+ }
62
+ }
63
+ if (failures.length === 0 && warnings.length === 0) {
64
+ console.log(` ${green('All checks passed.')}`);
65
+ }
66
+ console.log('');
67
+ }
68
+ export const doctorCommand = new Command('doctor')
69
+ .description('Check system health: auth, network, project config, agent setup')
70
+ .action(async (_opts, command) => {
71
+ const asJson = shouldOutputJson(command.optsWithGlobals().json);
72
+ const report = await runAllChecks();
73
+ if (asJson) {
74
+ jsonOut(report);
75
+ }
76
+ else {
77
+ printHumanReport(report);
78
+ }
79
+ if (report.checks.some(c => c.status === 'fail')) {
80
+ process.exit(1);
81
+ }
82
+ });
@@ -2,6 +2,7 @@ import { Command } from 'commander';
2
2
  import { readFileSync, existsSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { shouldOutputJson, jsonOut } from '../utils/output.js';
5
+ import { buildInfoGuide } from '../utils/agent-guidance.js';
5
6
  import { fetchAppInfo } from '../utils/app-info.js';
6
7
  import { bold, cyan, dim, green, yellow, gray } from '../ui/colors.js';
7
8
  import { VERSION } from '../generated/version.js';
@@ -186,7 +187,10 @@ export function formatSources(sources) {
186
187
  function printHumanOutput(data) {
187
188
  console.log('');
188
189
  const pad = (label) => label.padEnd(12);
189
- console.log(` ${dim(pad('Workspace:'))}${data.workspace.name || data.workspace.id}`);
190
+ const wsLabel = data.workspace.name && data.workspace.name !== data.workspace.id
191
+ ? `${data.workspace.name} ${dim('(' + data.workspace.id + ')')}`
192
+ : data.workspace.id;
193
+ console.log(` ${dim(pad('Workspace:'))}${wsLabel}`);
190
194
  if (data.app) {
191
195
  console.log('');
192
196
  console.log(bold('App Info'));
@@ -368,7 +372,7 @@ export const infoCommand = new Command('info')
368
372
  },
369
373
  };
370
374
  if (asJson) {
371
- jsonOut(output);
375
+ jsonOut({ ...output, guide: buildInfoGuide() });
372
376
  }
373
377
  else {
374
378
  printHumanOutput(output);
@@ -1,7 +1,15 @@
1
1
  import { Command } from 'commander';
2
2
  import { ApiClient } from '../api/client.js';
3
3
  import type { WorkspaceInfo } from '../types.js';
4
- export declare function execInit(client: ApiClient, appName: string, workspace: WorkspaceInfo): Promise<string>;
4
+ export interface InitResult {
5
+ appId: string;
6
+ appName: string;
7
+ slug: string;
8
+ directory: string;
9
+ workspaceId: string;
10
+ workspaceName: string;
11
+ }
12
+ export declare function execInit(client: ApiClient, appName: string, workspace: WorkspaceInfo): Promise<InitResult>;
5
13
  /** Full create flow: prompt for name/workspace, init, and run agent wizard */
6
- export declare function runCreateFlow(name?: string): Promise<void>;
14
+ export declare function runCreateFlow(name?: string, workspaceFlag?: string): Promise<void>;
7
15
  export declare const initCommand: Command;
@@ -5,10 +5,13 @@ import { join, resolve } from 'path';
5
5
  import { requireAuth } from '../auth/store.js';
6
6
  import { ApiClient } from '../api/client.js';
7
7
  import { promptSelect, promptInput } from '../utils/prompt.js';
8
+ import { resolveWorkspace } from '../utils/resolve.js';
8
9
  import { generateManifest, saveManifest } from '../template/manifest.js';
9
10
  import { extractZip } from '../utils/zip.js';
10
11
  import { removeNestedGitDirs } from '../utils/fs.js';
11
12
  import { runAgentWizard } from '../ui/banner.js';
13
+ import { shouldOutputJson, jsonOut } from '../utils/output.js';
14
+ import { buildInitGuide, buildErrorResponse } from '../utils/agent-guidance.js';
12
15
  export async function execInit(client, appName, workspace) {
13
16
  const app = await client.initApp(workspace.id, appName);
14
17
  const slug = app.slug || appName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
@@ -42,7 +45,7 @@ export async function execInit(client, appName, workspace) {
42
45
  writeFileSync(join(dir, '.runwork.json'), JSON.stringify(config, null, 2));
43
46
  // Initialize git
44
47
  if (!existsSync(join(dir, '.git'))) {
45
- execFileSync('git', ['init'], { cwd: dir, stdio: 'inherit' });
48
+ execFileSync('git', ['init'], { cwd: dir, stdio: 'pipe' });
46
49
  }
47
50
  // Add runwork remote
48
51
  const remoteUrl = client.getGitRemoteUrl(workspace.id, app.id);
@@ -64,34 +67,74 @@ export async function execInit(client, appName, workspace) {
64
67
  catch {
65
68
  // Nothing to commit is fine
66
69
  }
67
- execFileSync('git', ['push', '-u', 'runwork', 'main'], { cwd: dir, stdio: 'inherit' });
70
+ execFileSync('git', ['push', '-u', 'runwork', 'main'], { cwd: dir, stdio: 'pipe' });
68
71
  console.log(`\nApp "${app.name}" initialized in ${dir}/`);
69
- return resolve(dir);
72
+ return {
73
+ appId: app.id,
74
+ appName: app.name,
75
+ slug,
76
+ directory: resolve(dir),
77
+ workspaceId: workspace.id,
78
+ workspaceName: workspace.name,
79
+ };
70
80
  }
71
81
  /** Full create flow: prompt for name/workspace, init, and run agent wizard */
72
- export async function runCreateFlow(name) {
82
+ export async function runCreateFlow(name, workspaceFlag) {
83
+ const useJson = shouldOutputJson(undefined);
73
84
  const creds = requireAuth();
74
85
  const client = new ApiClient(creds);
75
86
  const appName = name || await promptInput('App name');
76
87
  if (!appName) {
88
+ if (useJson) {
89
+ jsonOut(buildErrorResponse('init', 'App name is required.', 'No app name was provided.', ['Provide an app name as an argument: runwork init <name>']));
90
+ return;
91
+ }
77
92
  console.error('App name is required.');
78
93
  process.exit(1);
79
94
  }
80
- const workspaces = await client.listWorkspaces();
81
- if (workspaces.length === 0) {
82
- console.error('No workspaces found. Create one at runwork.ai first.');
83
- process.exit(1);
95
+ let workspace;
96
+ if (workspaceFlag) {
97
+ workspace = await resolveWorkspace(client, workspaceFlag);
98
+ }
99
+ else {
100
+ const workspaces = await client.listWorkspaces();
101
+ if (workspaces.length === 0) {
102
+ if (useJson) {
103
+ jsonOut(buildErrorResponse('init', 'No workspaces found.', 'The authenticated account has no workspaces.', ['Create a workspace at runwork.ai first.']));
104
+ return;
105
+ }
106
+ console.error('No workspaces found. Create one at runwork.ai first.');
107
+ process.exit(1);
108
+ }
109
+ if (workspaces.length === 1) {
110
+ workspace = workspaces[0];
111
+ }
112
+ else {
113
+ const workspaceChoice = await promptSelect('Select workspace:', workspaces.map(w => ({ label: w.name, value: w })));
114
+ workspace = workspaceChoice.value;
115
+ }
116
+ }
117
+ if (!useJson) {
118
+ console.log(`Creating "${appName}" in workspace "${workspace.name}"...`);
119
+ }
120
+ const initResult = await execInit(client, appName, workspace);
121
+ if (useJson) {
122
+ const response = {
123
+ success: true,
124
+ command: 'init',
125
+ result: initResult,
126
+ guide: buildInitGuide(initResult.appName, initResult.slug),
127
+ };
128
+ jsonOut(response);
129
+ return;
84
130
  }
85
- const workspaceChoice = await promptSelect('Select workspace:', workspaces.map(w => ({ label: w.name, value: w })));
86
- const workspace = workspaceChoice.value;
87
- console.log(`Creating "${appName}" in workspace "${workspace.name}"...`);
88
- const dir = await execInit(client, appName, workspace);
89
- await runAgentWizard(dir);
131
+ await runAgentWizard(initResult.directory);
90
132
  }
91
133
  export const initCommand = new Command('init')
92
134
  .alias('create')
93
135
  .description('Create a new Runwork app')
94
136
  .argument('[name]', 'App name')
95
- .action(async (name) => {
96
- await runCreateFlow(name);
137
+ .option('--workspace <name-or-id>', 'Workspace name or ID (skips interactive selection)')
138
+ .action(async (name, options) => {
139
+ await runCreateFlow(name, options?.workspace);
97
140
  });