runwork 0.10.1 → 0.10.3

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 (47) hide show
  1. package/dist/commands/__tests__/clone-args.test.d.ts +1 -0
  2. package/dist/commands/__tests__/clone-args.test.js +44 -0
  3. package/dist/commands/clone.d.ts +14 -0
  4. package/dist/commands/clone.js +20 -2
  5. package/dist/commands/dev.d.ts +3 -0
  6. package/dist/commands/dev.js +621 -8
  7. package/dist/commands/info.d.ts +31 -0
  8. package/dist/commands/info.js +37 -0
  9. package/dist/dev/__tests__/attach.test.d.ts +1 -0
  10. package/dist/dev/__tests__/attach.test.js +296 -0
  11. package/dist/dev/__tests__/detach.test.d.ts +1 -0
  12. package/dist/dev/__tests__/detach.test.js +328 -0
  13. package/dist/dev/__tests__/preview-url-poller.test.d.ts +1 -0
  14. package/dist/dev/__tests__/preview-url-poller.test.js +149 -0
  15. package/dist/dev/__tests__/session.test.d.ts +1 -0
  16. package/dist/dev/__tests__/session.test.js +347 -0
  17. package/dist/dev/__tests__/stop.test.d.ts +1 -0
  18. package/dist/dev/__tests__/stop.test.js +172 -0
  19. package/dist/dev/attach.d.ts +120 -0
  20. package/dist/dev/attach.js +269 -0
  21. package/dist/dev/detach.d.ts +164 -0
  22. package/dist/dev/detach.js +247 -0
  23. package/dist/dev/preview-url-poller.d.ts +35 -0
  24. package/dist/dev/preview-url-poller.js +50 -0
  25. package/dist/dev/session.d.ts +158 -0
  26. package/dist/dev/session.js +252 -0
  27. package/dist/dev/stop.d.ts +52 -0
  28. package/dist/dev/stop.js +101 -0
  29. package/dist/generated/version.d.ts +1 -1
  30. package/dist/generated/version.js +1 -1
  31. package/dist/git/__tests__/credential-helper-e2e.test.d.ts +21 -0
  32. package/dist/git/__tests__/credential-helper-e2e.test.js +195 -0
  33. package/dist/git/__tests__/credentials.test.js +33 -20
  34. package/dist/git/__tests__/preflight-resolution.test.d.ts +1 -0
  35. package/dist/git/__tests__/preflight-resolution.test.js +366 -0
  36. package/dist/git/credentials.d.ts +17 -0
  37. package/dist/git/credentials.js +22 -1
  38. package/dist/git/preflight.d.ts +34 -5
  39. package/dist/git/preflight.js +237 -11
  40. package/dist/health/__tests__/checks.test.js +134 -0
  41. package/dist/health/checks.d.ts +13 -0
  42. package/dist/health/checks.js +130 -14
  43. package/dist/health/runner.js +5 -1
  44. package/dist/ui/__tests__/keyboard.test.js +4 -0
  45. package/dist/ui/keyboard.d.ts +1 -1
  46. package/dist/ui/keyboard.js +4 -0
  47. package/package.json +1 -1
@@ -7,6 +7,7 @@ import { getCredentials } from '../auth/store.js';
7
7
  import { ApiClient } from '../api/client.js';
8
8
  import { detectAgents, getAdapterBySlug } from '../agents/detect.js';
9
9
  import { httpFetch } from '../utils/http.js';
10
+ import { probeGit, parseGitVersion } from '../git/preflight.js';
10
11
  // First-party distribution endpoints. Defaults match the plan, can be
11
12
  // overridden via RUNWORK_DOWNLOAD_BASE_URL for staging or debugging.
12
13
  const BASE_URL = process.env.RUNWORK_DOWNLOAD_BASE_URL || 'https://runwork.ai';
@@ -50,26 +51,39 @@ export function buildContext() {
50
51
  }
51
52
  // ── 1. System dependencies ───────────────────────────────────────────
52
53
  export async function checkSystemDeps() {
53
- try {
54
- const output = execFileSync('git', ['--version'], { stdio: 'pipe' }).toString().trim();
55
- const match = output.match(/(\d+\.\d+(?:\.\d+)?)/);
56
- const version = match ? match[1] : 'unknown';
57
- if (match) {
58
- const [major, minor] = version.split('.').map(Number);
59
- if (major < 2 || (major === 2 && minor < 20)) {
60
- return { name: 'system-deps', status: 'warn', message: `git ${version} (old -- consider upgrading)` };
61
- }
62
- }
63
- return { name: 'system-deps', status: 'pass', message: `git ${version}` };
64
- }
65
- catch {
54
+ // Delegate to probeGit() so doctor agrees with the preflight gate used by
55
+ // `runwork clone`/`init`/`dev`. probeGit also has Windows fallback paths
56
+ // (`where.exe`, canonical install dirs) that the previous bare execFileSync
57
+ // missed when Bun's standalone-binary spawn lookup failed despite git
58
+ // being installed.
59
+ const probe = probeGit();
60
+ if (!probe.installed) {
61
+ const detail = probe.error?.message ? ` (${probe.error.message})` : '';
66
62
  return {
67
63
  name: 'system-deps',
68
64
  status: 'fail',
69
- message: 'git not found in PATH',
65
+ message: `git not found in PATH${detail}`,
70
66
  fix: 'Install git: https://git-scm.com/downloads',
71
67
  };
72
68
  }
69
+ const version = parseGitVersion(probe.version) ?? 'unknown';
70
+ // Show how we found it on Windows fallback paths so the user can see
71
+ // whether their PATH is wired up or we needed a rescue.
72
+ const sourceSuffix = probe.source === 'where' ? ` (resolved via where.exe at ${probe.path})`
73
+ : probe.source === 'registry' ? ` (resolved via Git for Windows registry at ${probe.path})`
74
+ : probe.source === 'canonical' ? ` (resolved via canonical install path ${probe.path})`
75
+ : '';
76
+ if (version !== 'unknown') {
77
+ const [major, minor] = version.split('.').map(Number);
78
+ if (major < 2 || (major === 2 && minor < 20)) {
79
+ return {
80
+ name: 'system-deps',
81
+ status: 'warn',
82
+ message: `git ${version} (old -- consider upgrading)${sourceSuffix}`,
83
+ };
84
+ }
85
+ }
86
+ return { name: 'system-deps', status: 'pass', message: `git ${version}${sourceSuffix}` };
73
87
  }
74
88
  // ── 2. CLI version ───────────────────────────────────────────────────
75
89
  export async function checkCliVersion() {
@@ -224,6 +238,108 @@ export async function checkAuthAndNetwork(ctx) {
224
238
  };
225
239
  }
226
240
  }
241
+ // ── 4a. Git credential helper registration ──────────────────────────
242
+ /**
243
+ * Verify that git's credential helper for the runwork origin is registered
244
+ * and that the binary it points at exists on disk. Catches the very common
245
+ * "logged in with an older CLI before configureGitCredentials shipped"
246
+ * state, where the user has credentials but git falls back to the system
247
+ * credential manager (Git Credential Manager popup on Windows, etc.) when
248
+ * trying to clone or push -- producing confusing UX with no obvious fix.
249
+ *
250
+ * The check is scoped to the *user's* logged-in baseUrl when available, so
251
+ * it doesn't false-fail on staging deployments that point at a different
252
+ * origin.
253
+ */
254
+ export async function checkGitCredentialHelper(ctx) {
255
+ if (!ctx.credentials) {
256
+ return {
257
+ name: 'git-credential-helper',
258
+ status: 'skip',
259
+ message: 'skipped (not logged in)',
260
+ };
261
+ }
262
+ // Make sure git itself is callable before we try to read its config --
263
+ // probeGit() prepends the resolved git directory to PATH for us when it
264
+ // resolves via fallback, so this also smooths the way for the
265
+ // execFileSync('git', ...) call below.
266
+ const git = probeGit();
267
+ if (!git.installed) {
268
+ return {
269
+ name: 'git-credential-helper',
270
+ status: 'skip',
271
+ message: 'skipped (git not installed)',
272
+ };
273
+ }
274
+ let origin;
275
+ try {
276
+ origin = new URL(ctx.credentials.baseUrl).origin;
277
+ }
278
+ catch {
279
+ return {
280
+ name: 'git-credential-helper',
281
+ status: 'fail',
282
+ message: `cannot parse baseUrl from credentials: ${ctx.credentials.baseUrl}`,
283
+ fix: 'runwork login',
284
+ };
285
+ }
286
+ // `--get-regexp` returns 1 (and prints nothing) when no entry matches,
287
+ // so a non-zero exit is a normal "not registered" signal -- not an error.
288
+ let helperConfig;
289
+ try {
290
+ const buf = execFileSync('git', ['config', '--global', '--get-regexp', 'credential\\..*runwork.*\\.helper'], { stdio: ['ignore', 'pipe', 'ignore'] });
291
+ helperConfig = buf.toString('utf-8');
292
+ }
293
+ catch {
294
+ return {
295
+ name: 'git-credential-helper',
296
+ status: 'fail',
297
+ message: `no credential helper registered for ${origin}`,
298
+ fix: 'runwork login',
299
+ };
300
+ }
301
+ // Each line is `credential.<scope>.helper <value>`. We want a line whose
302
+ // <scope> matches the user's actual baseUrl origin (not e.g. a stale
303
+ // staging origin from a previous login).
304
+ const lines = helperConfig.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
305
+ const escapedOrigin = origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
306
+ const scopedRegex = new RegExp(`^credential\\.${escapedOrigin}\\.helper\\s+(.+)$`);
307
+ const matchedLine = lines.find(line => scopedRegex.test(line));
308
+ if (!matchedLine) {
309
+ return {
310
+ name: 'git-credential-helper',
311
+ status: 'fail',
312
+ message: `no credential helper registered for ${origin} (found ${lines.length} other runwork helper entries)`,
313
+ fix: 'runwork login',
314
+ };
315
+ }
316
+ const value = scopedRegex.exec(matchedLine)?.[1] ?? '';
317
+ // Helper values starting with `!` are shell commands. Common shapes:
318
+ // `!runwork git-credential-helper` (PATH-relative)
319
+ // `!"/abs/path/to/runwork" git-credential-helper` (post-Pass-2 hardening, POSIX)
320
+ // `!"C:/.../runwork.exe" git-credential-helper` (post-Pass-2 hardening, Windows)
321
+ // We only validate "absolute path" entries by existsSync() because that's
322
+ // the unambiguous case. PATH-relative entries we trust git+bash to resolve
323
+ // at invocation time (no good way to simulate that without running it).
324
+ const absoluteHelperRegex = /^!"?(\/|[A-Za-z]:[\\/])/;
325
+ if (absoluteHelperRegex.test(value)) {
326
+ const pathMatch = value.match(/^!"([^"]+)"|^!(\S+)/);
327
+ const helperPath = pathMatch?.[1] ?? pathMatch?.[2] ?? '';
328
+ if (helperPath && !existsSync(helperPath)) {
329
+ return {
330
+ name: 'git-credential-helper',
331
+ status: 'fail',
332
+ message: `helper registered but binary not found at ${helperPath}`,
333
+ fix: 'runwork login (re-registers with the current binary path)',
334
+ };
335
+ }
336
+ }
337
+ return {
338
+ name: 'git-credential-helper',
339
+ status: 'pass',
340
+ message: `registered for ${origin}`,
341
+ };
342
+ }
227
343
  // ── 5. Project config ────────────────────────────────────────────────
228
344
  export async function checkProjectConfig(ctx) {
229
345
  const configPath = join(ctx.cwd, '.runwork.json');
@@ -1,4 +1,4 @@
1
- import { buildContext, checkSystemDeps, checkCliVersion, checkCliArtifactReachable, checkCliInstallLocation, checkAuthAndNetwork, checkProjectConfig, checkAppExists, checkGitRemote, checkAgentSetup, } from './checks.js';
1
+ import { buildContext, checkSystemDeps, checkCliVersion, checkCliArtifactReachable, checkCliInstallLocation, checkAuthAndNetwork, checkGitCredentialHelper, checkProjectConfig, checkAppExists, checkGitRemote, checkAgentSetup, } from './checks.js';
2
2
  export async function runAllChecks() {
3
3
  const ctx = buildContext();
4
4
  const checks = [];
@@ -14,6 +14,10 @@ export async function runAllChecks() {
14
14
  const { auth, network } = await checkAuthAndNetwork(ctx);
15
15
  checks.push(auth);
16
16
  checks.push(network);
17
+ // 4a. Git credential helper -- catches "logged in but git auth not wired
18
+ // up" state that surfaces on Windows as a Git Credential Manager
19
+ // popup mid-clone with no obvious cause.
20
+ checks.push(await checkGitCredentialHelper(ctx));
17
21
  // 5. Project config
18
22
  checks.push(await checkProjectConfig(ctx));
19
23
  // 6. App exists
@@ -27,4 +27,8 @@ describe('parseKeypress', () => {
27
27
  it('detects "i" for info', () => {
28
28
  expect(parseKeypress(Buffer.from('i'))).toBe('i');
29
29
  });
30
+ it('detects "s" for stop (used by `runwork dev attach`)', () => {
31
+ expect(parseKeypress(Buffer.from('s'))).toBe('s');
32
+ expect(parseKeypress(Buffer.from('S'))).toBe('s');
33
+ });
30
34
  });
@@ -1,4 +1,4 @@
1
- export type KeyAction = 'o' | 'p' | 'a' | 'e' | 'r' | 'i' | 'quit' | null;
1
+ export type KeyAction = 'o' | 'p' | 'a' | 'e' | 'r' | 'i' | 's' | 'quit' | null;
2
2
  /** Parse a raw stdin buffer into a named action. */
3
3
  export declare function parseKeypress(data: Buffer): KeyAction;
4
4
  export interface KeyboardListener {
@@ -12,6 +12,10 @@ export function parseKeypress(data) {
12
12
  case 'e': return 'e';
13
13
  case 'r': return 'r';
14
14
  case 'i': return 'i';
15
+ // 's' is consumed by `runwork dev attach` to stop the attached
16
+ // session. Other commands ignore it. We parse it centrally so the
17
+ // raw keypress doesn't leak through as a `null` action.
18
+ case 's': return 's';
15
19
  case 'q': return 'quit';
16
20
  default: return null;
17
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runwork",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "CLI for Runwork: develop, preview, and deploy Runwork apps from your local machine.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Runwork <info@runwork.ai> (https://www.runwork.ai)",