runwork 0.13.4 → 0.15.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 (95) hide show
  1. package/bundled-types/core-endpoints.d.ts +16 -4
  2. package/dist/__tests__/install-scripts.test.js +33 -0
  3. package/dist/agents/__tests__/chatgpt-registry.test.d.ts +1 -0
  4. package/dist/agents/__tests__/chatgpt-registry.test.js +61 -0
  5. package/dist/agents/__tests__/detection.test.js +3 -0
  6. package/dist/agents/claude-desktop-plugin-tree.js +23 -8
  7. package/dist/agents/claude-desktop.js +6 -3
  8. package/dist/agents/codex.js +15 -7
  9. package/dist/agents/detection.js +3 -0
  10. package/dist/agents/intro-skill.js +5 -3
  11. package/dist/agents/registry-data.d.ts +59 -2
  12. package/dist/agents/registry-data.js +176 -0
  13. package/dist/api/client.d.ts +5 -0
  14. package/dist/api/client.js +8 -0
  15. package/dist/commands/__tests__/clone-args.test.js +8 -1
  16. package/dist/commands/__tests__/clone-source-result.test.d.ts +1 -0
  17. package/dist/commands/__tests__/clone-source-result.test.js +45 -0
  18. package/dist/commands/__tests__/mcp-entries.test.d.ts +1 -0
  19. package/dist/commands/__tests__/mcp-entries.test.js +48 -0
  20. package/dist/commands/clone.d.ts +33 -0
  21. package/dist/commands/clone.js +105 -10
  22. package/dist/commands/deploy.js +128 -11
  23. package/dist/commands/dev.js +72 -32
  24. package/dist/commands/doctor.js +64 -3
  25. package/dist/commands/info.d.ts +3 -0
  26. package/dist/commands/info.js +11 -0
  27. package/dist/commands/mcp-entries.d.ts +13 -0
  28. package/dist/commands/mcp-entries.js +26 -0
  29. package/dist/commands/sync.js +20 -11
  30. package/dist/commands/validate.d.ts +2 -0
  31. package/dist/commands/validate.js +89 -0
  32. package/dist/deploy/__tests__/deploy-state.test.d.ts +1 -0
  33. package/dist/deploy/__tests__/deploy-state.test.js +80 -0
  34. package/dist/deploy/__tests__/deploy-status.test.d.ts +1 -0
  35. package/dist/deploy/__tests__/deploy-status.test.js +41 -0
  36. package/dist/deploy/__tests__/detach-args.test.d.ts +1 -0
  37. package/dist/deploy/__tests__/detach-args.test.js +30 -0
  38. package/dist/deploy/deploy-state.d.ts +36 -0
  39. package/dist/deploy/deploy-state.js +63 -0
  40. package/dist/deploy/deploy-status.d.ts +21 -0
  41. package/dist/deploy/deploy-status.js +36 -0
  42. package/dist/deploy/detach.d.ts +32 -0
  43. package/dist/deploy/detach.js +71 -0
  44. package/dist/dev/__tests__/session.test.js +39 -0
  45. package/dist/dev/__tests__/startup-sync.test.d.ts +1 -0
  46. package/dist/dev/__tests__/startup-sync.test.js +32 -0
  47. package/dist/dev/session.d.ts +9 -0
  48. package/dist/dev/session.js +4 -1
  49. package/dist/dev/startup-sync.d.ts +23 -0
  50. package/dist/dev/startup-sync.js +14 -0
  51. package/dist/generated/bundled-types.js +1 -1
  52. package/dist/generated/version.d.ts +1 -1
  53. package/dist/generated/version.js +1 -1
  54. package/dist/git/__tests__/classify-sync-error.test.d.ts +1 -0
  55. package/dist/git/__tests__/classify-sync-error.test.js +57 -0
  56. package/dist/git/__tests__/ensure-git-credential-helper.test.d.ts +1 -0
  57. package/dist/git/__tests__/ensure-git-credential-helper.test.js +133 -0
  58. package/dist/git/__tests__/ensure-runwork-remote.test.d.ts +1 -0
  59. package/dist/git/__tests__/ensure-runwork-remote.test.js +109 -0
  60. package/dist/git/__tests__/repo-config.test.d.ts +1 -0
  61. package/dist/git/__tests__/repo-config.test.js +26 -0
  62. package/dist/git/classify-sync-error.d.ts +21 -0
  63. package/dist/git/classify-sync-error.js +111 -0
  64. package/dist/git/credentials.d.ts +43 -0
  65. package/dist/git/credentials.js +67 -0
  66. package/dist/git/remote.d.ts +37 -0
  67. package/dist/git/remote.js +75 -0
  68. package/dist/git/repo-config.d.ts +8 -0
  69. package/dist/git/repo-config.js +29 -0
  70. package/dist/health/__tests__/checks.test.js +2 -2
  71. package/dist/health/__tests__/fix.test.d.ts +1 -0
  72. package/dist/health/__tests__/fix.test.js +72 -0
  73. package/dist/health/__tests__/runner-filter.test.d.ts +1 -0
  74. package/dist/health/__tests__/runner-filter.test.js +61 -0
  75. package/dist/health/checks.d.ts +7 -0
  76. package/dist/health/checks.js +50 -36
  77. package/dist/health/fix.d.ts +22 -0
  78. package/dist/health/fix.js +62 -0
  79. package/dist/health/runner.d.ts +7 -1
  80. package/dist/health/runner.js +40 -27
  81. package/dist/index.js +2 -0
  82. package/dist/tools/types.d.ts +5 -0
  83. package/dist/types.d.ts +3 -0
  84. package/dist/utils/__tests__/help-json.test.d.ts +1 -0
  85. package/dist/utils/__tests__/help-json.test.js +39 -0
  86. package/dist/utils/__tests__/ignore-matcher.test.js +18 -1
  87. package/dist/utils/help-json.d.ts +14 -0
  88. package/dist/utils/help-json.js +4 -1
  89. package/dist/validate/__tests__/validate.test.d.ts +1 -0
  90. package/dist/validate/__tests__/validate.test.js +42 -0
  91. package/dist/validate/freshness.d.ts +28 -0
  92. package/dist/validate/freshness.js +48 -0
  93. package/dist/validate/preview.d.ts +21 -0
  94. package/dist/validate/preview.js +39 -0
  95. package/package.json +1 -1
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Command } from 'commander';
3
+ import { buildCommandHelpJson } from '../help-json.js';
4
+ function optByFlags(cmd, flags) {
5
+ const json = buildCommandHelpJson(cmd, '1.0.0');
6
+ const opt = json.options.find(o => o.flags === flags);
7
+ if (!opt)
8
+ throw new Error(`option ${flags} not found`);
9
+ return opt;
10
+ }
11
+ describe('help-json option required semantics', () => {
12
+ const cmd = new Command('demo')
13
+ .option('--workspace <id>', 'workspace id (optional; defaults from credentials)')
14
+ .option('--flag', 'a boolean flag')
15
+ .option('--maybe [value]', 'an optional value')
16
+ .requiredOption('--token <t>', 'a genuinely required option');
17
+ it('marks a value-taking-but-optional option as NOT required (the --workspace bug)', () => {
18
+ const o = optByFlags(cmd, '--workspace <id>');
19
+ expect(o.required).toBe(false); // the option itself is not mandatory
20
+ expect(o.valueRequired).toBe(true); // but its value is required when present
21
+ expect(o.optional).toBe(false);
22
+ });
23
+ it('marks a boolean flag as neither required nor value-required', () => {
24
+ const o = optByFlags(cmd, '--flag');
25
+ expect(o.required).toBe(false);
26
+ expect(o.valueRequired).toBe(false);
27
+ });
28
+ it('marks an optional-value option correctly', () => {
29
+ const o = optByFlags(cmd, '--maybe [value]');
30
+ expect(o.required).toBe(false);
31
+ expect(o.valueRequired).toBe(false);
32
+ expect(o.optional).toBe(true);
33
+ });
34
+ it('marks a genuinely mandatory option as required AND value-required', () => {
35
+ const o = optByFlags(cmd, '--token <t>');
36
+ expect(o.required).toBe(true);
37
+ expect(o.valueRequired).toBe(true);
38
+ });
39
+ });
@@ -2,7 +2,7 @@ import { describe, it, expect, afterEach } from 'vitest';
2
2
  import { mkdtempSync, writeFileSync, rmSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { tmpdir } from 'os';
5
- import { buildIgnoreSets, defaultIgnoreSets, isPathIgnored, parseGitignoreContent, } from '../ignore-matcher.js';
5
+ import { buildIgnoreSets, defaultIgnoreSets, isPathIgnored, isRelPathIgnored, parseGitignoreContent, } from '../ignore-matcher.js';
6
6
  const tempDirs = [];
7
7
  function makeTempDir(prefix) {
8
8
  const dir = mkdtempSync(join(tmpdir(), `runwork-ignore-${prefix}-`));
@@ -26,6 +26,23 @@ describe('defaultIgnoreSets()', () => {
26
26
  expect(sets.dirs.has('.runwork')).toBe(true);
27
27
  expect(sets.dirs.has('.bun-cache')).toBe(true);
28
28
  });
29
+ it('excludes every .runwork/ state file from deploy/dev auto-commit staging', () => {
30
+ // commitWorkingTree() filters untracked files through isRelPathIgnored,
31
+ // so any path with a .runwork segment must be excluded -- including the
32
+ // local deploy-state/status files written before the detached child
33
+ // commits (Codex review P1).
34
+ const sets = defaultIgnoreSets();
35
+ for (const p of [
36
+ '.runwork/deploy-status.json',
37
+ '.runwork/last-deploy.json',
38
+ '.runwork/deploy-stdout.log',
39
+ '.runwork/dev-session.json',
40
+ '.runwork/setup.json',
41
+ '.runwork/preview-url',
42
+ ]) {
43
+ expect(isRelPathIgnored(p, sets), `${p} must be ignored`).toBe(true);
44
+ }
45
+ });
29
46
  it('includes .dev.vars and .env file defaults', () => {
30
47
  const sets = defaultIgnoreSets();
31
48
  expect(sets.files.has('.dev.vars')).toBe(true);
@@ -11,7 +11,21 @@ import type { Command } from 'commander';
11
11
  interface JsonOption {
12
12
  flags: string;
13
13
  description: string;
14
+ /**
15
+ * Whether the OPTION itself must be supplied (Commander's `mandatory`, set
16
+ * via `.makeOptionMandatory()`). This is what an agent reasoning about
17
+ * "do I have to pass this?" wants. Most options are false here even when
18
+ * they take a value -- e.g. `--workspace <id>` is optional (defaults from
19
+ * credentials). Previously this field was wrongly mapped to Commander's
20
+ * `required` (value-required), which marked nearly every option as required.
21
+ */
14
22
  required: boolean;
23
+ /**
24
+ * Whether the option's VALUE is required when the flag is present, i.e. it
25
+ * was declared with `<value>` (vs `[value]` or a boolean flag). Independent
26
+ * of whether the option itself must be supplied.
27
+ */
28
+ valueRequired: boolean;
15
29
  optional: boolean;
16
30
  variadic: boolean;
17
31
  defaultValue?: unknown;
@@ -11,7 +11,10 @@ function serializeOption(opt) {
11
11
  const result = {
12
12
  flags: opt.flags,
13
13
  description: opt.description,
14
- required: opt.required,
14
+ // Commander's `mandatory` = the option must be supplied; `required` = the
15
+ // option's value is required when present. Expose both, correctly named.
16
+ required: opt.mandatory,
17
+ valueRequired: opt.required,
15
18
  optional: opt.optional,
16
19
  variadic: opt.variadic,
17
20
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { summarizeFreshness } from '../freshness.js';
3
+ import { detectViteError } from '../preview.js';
4
+ describe('validate/freshness summarizeFreshness()', () => {
5
+ const base = { localSha: 'a', remoteSha: 'a', ahead: 0, behind: 0, uncommitted: false };
6
+ it('is in-sync when local==remote, no ahead/behind, clean tree', () => {
7
+ expect(summarizeFreshness(base).inSync).toBe(true);
8
+ });
9
+ it('is not in-sync with uncommitted changes', () => {
10
+ expect(summarizeFreshness({ ...base, uncommitted: true }).inSync).toBe(false);
11
+ });
12
+ it('is not in-sync when ahead of the remote', () => {
13
+ expect(summarizeFreshness({ ...base, ahead: 2 }).inSync).toBe(false);
14
+ });
15
+ it('is not in-sync when behind the remote', () => {
16
+ expect(summarizeFreshness({ ...base, behind: 1 }).inSync).toBe(false);
17
+ });
18
+ it('is not in-sync when there is no remote ref', () => {
19
+ expect(summarizeFreshness({ ...base, remoteSha: null }).inSync).toBe(false);
20
+ });
21
+ it('is not in-sync when there is no local HEAD', () => {
22
+ expect(summarizeFreshness({ ...base, localSha: null }).inSync).toBe(false);
23
+ });
24
+ it('passes through the raw counts', () => {
25
+ const f = summarizeFreshness({ localSha: 'x', remoteSha: 'y', ahead: 3, behind: 1, uncommitted: true });
26
+ expect(f).toMatchObject({ ahead: 3, behind: 1, uncommitted: true, inSync: false });
27
+ });
28
+ });
29
+ describe('validate/preview detectViteError()', () => {
30
+ it('detects the Vite error overlay element', () => {
31
+ expect(detectViteError('<vite-error-overlay></vite-error-overlay>')).toBe(true);
32
+ });
33
+ it('detects a plugin transform error', () => {
34
+ expect(detectViteError('[plugin:vite:react-babel] Unexpected token')).toBe(true);
35
+ });
36
+ it('detects a failed import resolution', () => {
37
+ expect(detectViteError('Failed to resolve import "./missing" from "src/App.tsx"')).toBe(true);
38
+ });
39
+ it('returns false for a healthy HTML document', () => {
40
+ expect(detectViteError('<!DOCTYPE html><html><body><div id="root"></div></body></html>')).toBe(false);
41
+ });
42
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Git sync-freshness: how the local working tree relates to the deploy remote
3
+ * (`runwork/main`). Answers "do I have changes that aren't synced/deployed?"
4
+ * without scraping the sandbox (friction log #10).
5
+ */
6
+ export interface Freshness {
7
+ localSha: string | null;
8
+ remoteSha: string | null;
9
+ ahead: number;
10
+ behind: number;
11
+ uncommitted: boolean;
12
+ /** true when local == remote and the tree is clean. */
13
+ inSync: boolean;
14
+ }
15
+ export interface FreshnessInput {
16
+ localSha: string | null;
17
+ remoteSha: string | null;
18
+ ahead: number;
19
+ behind: number;
20
+ uncommitted: boolean;
21
+ }
22
+ /** Pure: derive the in-sync verdict from the raw git counts/flags. */
23
+ export declare function summarizeFreshness(input: FreshnessInput): Freshness;
24
+ /**
25
+ * Collect git freshness for `cwd`. Best-effort fetches `runwork/main` first so
26
+ * the comparison reflects the real remote tip; tolerates offline/no-remote.
27
+ */
28
+ export declare function getGitFreshness(cwd: string): Freshness;
@@ -0,0 +1,48 @@
1
+ import { execFileSync } from '../utils/subprocess.js';
2
+ /** Pure: derive the in-sync verdict from the raw git counts/flags. */
3
+ export function summarizeFreshness(input) {
4
+ const inSync = input.localSha !== null &&
5
+ input.remoteSha !== null &&
6
+ input.ahead === 0 &&
7
+ input.behind === 0 &&
8
+ !input.uncommitted;
9
+ return { ...input, inSync };
10
+ }
11
+ function gitOut(cwd, args) {
12
+ try {
13
+ return execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ /**
20
+ * Collect git freshness for `cwd`. Best-effort fetches `runwork/main` first so
21
+ * the comparison reflects the real remote tip; tolerates offline/no-remote.
22
+ */
23
+ export function getGitFreshness(cwd) {
24
+ // Best-effort: refresh the remote-tracking ref. Failure (offline, no remote)
25
+ // just means we compare against whatever ref we already have.
26
+ try {
27
+ execFileSync('git', ['fetch', 'runwork', 'main'], { cwd, stdio: 'pipe' });
28
+ }
29
+ catch {
30
+ /* offline or no runwork remote -- non-fatal */
31
+ }
32
+ const localSha = gitOut(cwd, ['rev-parse', 'HEAD']);
33
+ const remoteSha = gitOut(cwd, ['rev-parse', 'runwork/main']);
34
+ let ahead = 0;
35
+ let behind = 0;
36
+ if (localSha && remoteSha) {
37
+ // `<behind>\t<ahead>` relative to runwork/main...HEAD
38
+ const counts = gitOut(cwd, ['rev-list', '--left-right', '--count', 'runwork/main...HEAD']);
39
+ if (counts) {
40
+ const [b, a] = counts.split(/\s+/).map(n => parseInt(n, 10));
41
+ behind = Number.isFinite(b) ? b : 0;
42
+ ahead = Number.isFinite(a) ? a : 0;
43
+ }
44
+ }
45
+ const porcelain = gitOut(cwd, ['status', '--porcelain']);
46
+ const uncommitted = porcelain !== null && porcelain.length > 0;
47
+ return summarizeFreshness({ localSha, remoteSha, ahead, behind, uncommitted });
48
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Liveness of the preview deployment: does it respond, and is it serving a
3
+ * working build (vs a Vite error overlay)? This is the cheap "is my change
4
+ * live and compiling" signal the friction log (#10) reached for by hand.
5
+ */
6
+ export interface PreviewLiveness {
7
+ url: string | null;
8
+ responds: boolean;
9
+ status: number | null;
10
+ /** A Vite/build error overlay was detected in the served HTML. */
11
+ errorDetected: boolean;
12
+ error?: string;
13
+ }
14
+ /**
15
+ * Heuristically detect a Vite dev-server error overlay / transform error in
16
+ * served HTML. These are the markers Vite injects when a module fails to
17
+ * compile, so their presence means the preview is serving a broken build.
18
+ */
19
+ export declare function detectViteError(html: string): boolean;
20
+ /** Fetch the preview URL and report whether it's live and error-free. */
21
+ export declare function checkPreview(url: string | null): Promise<PreviewLiveness>;
@@ -0,0 +1,39 @@
1
+ import { httpFetch } from '../utils/http.js';
2
+ /**
3
+ * Heuristically detect a Vite dev-server error overlay / transform error in
4
+ * served HTML. These are the markers Vite injects when a module fails to
5
+ * compile, so their presence means the preview is serving a broken build.
6
+ */
7
+ export function detectViteError(html) {
8
+ const markers = [
9
+ 'vite-error-overlay',
10
+ 'class="vite-error',
11
+ '[vite] Internal Server Error',
12
+ '[plugin:vite:',
13
+ 'Pre-transform error',
14
+ 'Failed to resolve import',
15
+ 'Internal server error:',
16
+ ];
17
+ return markers.some(m => html.includes(m));
18
+ }
19
+ /** Fetch the preview URL and report whether it's live and error-free. */
20
+ export async function checkPreview(url) {
21
+ if (!url) {
22
+ return { url: null, responds: false, status: null, errorDetected: false };
23
+ }
24
+ try {
25
+ const res = await httpFetch(url, { method: 'GET' });
26
+ let errorDetected = false;
27
+ try {
28
+ const body = await res.text();
29
+ errorDetected = detectViteError(body);
30
+ }
31
+ catch {
32
+ /* body read failed -- treat as no overlay detected */
33
+ }
34
+ return { url, responds: res.status > 0 && res.status < 500, status: res.status, errorDetected };
35
+ }
36
+ catch (err) {
37
+ return { url, responds: false, status: null, errorDetected: false, error: err instanceof Error ? err.message : String(err) };
38
+ }
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runwork",
3
- "version": "0.13.4",
3
+ "version": "0.15.0",
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)",