runwork 0.13.3 → 0.13.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Deploy guard: detect whether `runwork deploy` is about to commit and
3
+ * deploy code that the preview does not reflect, and decide whether to
4
+ * warn the user about it.
5
+ *
6
+ * The preview reflects the last *pushed* commit. When an auto-syncing
7
+ * `runwork dev` session is running, its file watcher commits and pushes
8
+ * continuously, so the working tree, the remote, and the preview stay in
9
+ * lockstep and there is nothing to warn about. But when there is no
10
+ * session, or the session is `--no-sync` (manual git), a dirty or
11
+ * ahead-of-remote working tree means the deploy will ship code the user
12
+ * never saw in the preview. That is the case we warn on.
13
+ *
14
+ * The pure decision (`shouldWarnBeforeDeploy`) is separated from the git
15
+ * and session IO so it can be unit-tested without a repo or session file.
16
+ */
17
+ import { type SessionState } from '../dev/session.js';
18
+ export interface WorkingTreeStatus {
19
+ /** `git status --porcelain` reported at least one entry (tracked or untracked). */
20
+ dirty: boolean;
21
+ /** Local HEAD has commits not present on `runwork/main`. */
22
+ ahead: boolean;
23
+ }
24
+ /**
25
+ * Whether the working tree diverges from what the preview shows.
26
+ * "Dirty" = uncommitted changes; "ahead" = committed but unpushed.
27
+ */
28
+ export declare function isDirtyOrAhead(status: WorkingTreeStatus): boolean;
29
+ /**
30
+ * Decide whether to warn before deploying.
31
+ *
32
+ * We warn only when the tree diverges from the preview AND no
33
+ * auto-syncing dev session is keeping things in lockstep. A live session
34
+ * whose `noSync` is not true owns commits/pushes, so it is suppressed; a
35
+ * `noSync` session (or no session at all) does not, so we warn.
36
+ */
37
+ export declare function shouldWarnBeforeDeploy(status: WorkingTreeStatus, session: SessionState): boolean;
38
+ /**
39
+ * Probe the working tree via git. `dirty` includes untracked files (a new
40
+ * file the user added is still not in the preview). `ahead` counts local
41
+ * commits not on `runwork/main`; a missing remote ref is treated as not
42
+ * ahead (no baseline to compare against, and the push step handles it).
43
+ */
44
+ export declare function readWorkingTreeStatus(cwd: string): WorkingTreeStatus;
45
+ /**
46
+ * End-to-end guard decision against the real repo and session file.
47
+ * Returns the status alongside the warn decision so callers can include
48
+ * the specifics (dirty / ahead) in their message.
49
+ */
50
+ export declare function evaluateDeployGuard(cwd: string, appId: string): {
51
+ status: WorkingTreeStatus;
52
+ warn: boolean;
53
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Deploy guard: detect whether `runwork deploy` is about to commit and
3
+ * deploy code that the preview does not reflect, and decide whether to
4
+ * warn the user about it.
5
+ *
6
+ * The preview reflects the last *pushed* commit. When an auto-syncing
7
+ * `runwork dev` session is running, its file watcher commits and pushes
8
+ * continuously, so the working tree, the remote, and the preview stay in
9
+ * lockstep and there is nothing to warn about. But when there is no
10
+ * session, or the session is `--no-sync` (manual git), a dirty or
11
+ * ahead-of-remote working tree means the deploy will ship code the user
12
+ * never saw in the preview. That is the case we warn on.
13
+ *
14
+ * The pure decision (`shouldWarnBeforeDeploy`) is separated from the git
15
+ * and session IO so it can be unit-tested without a repo or session file.
16
+ */
17
+ import { execFileSync } from '../utils/subprocess.js';
18
+ import { getSessionState } from '../dev/session.js';
19
+ /**
20
+ * Whether the working tree diverges from what the preview shows.
21
+ * "Dirty" = uncommitted changes; "ahead" = committed but unpushed.
22
+ */
23
+ export function isDirtyOrAhead(status) {
24
+ return status.dirty || status.ahead;
25
+ }
26
+ /**
27
+ * Decide whether to warn before deploying.
28
+ *
29
+ * We warn only when the tree diverges from the preview AND no
30
+ * auto-syncing dev session is keeping things in lockstep. A live session
31
+ * whose `noSync` is not true owns commits/pushes, so it is suppressed; a
32
+ * `noSync` session (or no session at all) does not, so we warn.
33
+ */
34
+ export function shouldWarnBeforeDeploy(status, session) {
35
+ if (!isDirtyOrAhead(status))
36
+ return false;
37
+ if (session.state === 'alive' && session.file.noSync !== true) {
38
+ // An auto-syncing watcher keeps the preview in lockstep with the tree.
39
+ return false;
40
+ }
41
+ return true;
42
+ }
43
+ /**
44
+ * Probe the working tree via git. `dirty` includes untracked files (a new
45
+ * file the user added is still not in the preview). `ahead` counts local
46
+ * commits not on `runwork/main`; a missing remote ref is treated as not
47
+ * ahead (no baseline to compare against, and the push step handles it).
48
+ */
49
+ export function readWorkingTreeStatus(cwd) {
50
+ let dirty = false;
51
+ try {
52
+ const porcelain = execFileSync('git', ['status', '--porcelain'], { cwd, encoding: 'utf-8' });
53
+ dirty = porcelain.trim().length > 0;
54
+ }
55
+ catch {
56
+ // Not a git repo yet, or git unavailable. Nothing meaningful to warn about.
57
+ }
58
+ let ahead = false;
59
+ try {
60
+ const count = execFileSync('git', ['rev-list', '--count', 'runwork/main..HEAD'], { cwd, encoding: 'utf-8' });
61
+ ahead = parseInt(count.trim(), 10) > 0;
62
+ }
63
+ catch {
64
+ // `runwork/main` does not exist locally (never fetched) or no HEAD yet.
65
+ // Treat as not ahead: there is no pushed baseline to diverge from.
66
+ }
67
+ return { dirty, ahead };
68
+ }
69
+ /**
70
+ * End-to-end guard decision against the real repo and session file.
71
+ * Returns the status alongside the warn decision so callers can include
72
+ * the specifics (dirty / ahead) in their message.
73
+ */
74
+ export function evaluateDeployGuard(cwd, appId) {
75
+ const status = readWorkingTreeStatus(cwd);
76
+ const session = getSessionState(cwd, appId);
77
+ return { status, warn: shouldWarnBeforeDeploy(status, session) };
78
+ }
@@ -21,6 +21,8 @@ export interface AgentResponse<T> {
21
21
  result: T;
22
22
  guide?: AgentGuide;
23
23
  error?: AgentError;
24
+ /** Non-blocking advisory surfaced alongside a successful result. */
25
+ warning?: string;
24
26
  }
25
27
  /** Map of key template files and what they're for. */
26
28
  export declare const APP_STRUCTURE: Record<string, string>;
@@ -35,4 +35,13 @@ export declare function buildIgnoreSets(dir: string): IgnoreSets;
35
35
  * recursive directory walkers without needing absolute-path bookkeeping.
36
36
  */
37
37
  export declare function isPathIgnored(filePath: string, sets: IgnoreSets): boolean;
38
+ /**
39
+ * Like isPathIgnored, but for a full repo-relative path (e.g. the output of
40
+ * `git ls-files`) rather than a single traversal step. It also treats a path
41
+ * as ignored when ANY parent directory segment is an ignored dir (e.g.
42
+ * `worker/.bun-cache/x.js`). The chokidar watcher gets this for free because
43
+ * it never descends into an ignored directory; a flat path list does not, so
44
+ * callers staging untracked files must use this variant to match the watcher.
45
+ */
46
+ export declare function isRelPathIgnored(relPath: string, sets: IgnoreSets): boolean;
38
47
  export {};
@@ -114,3 +114,19 @@ export function isPathIgnored(filePath, sets) {
114
114
  }
115
115
  return false;
116
116
  }
117
+ /**
118
+ * Like isPathIgnored, but for a full repo-relative path (e.g. the output of
119
+ * `git ls-files`) rather than a single traversal step. It also treats a path
120
+ * as ignored when ANY parent directory segment is an ignored dir (e.g.
121
+ * `worker/.bun-cache/x.js`). The chokidar watcher gets this for free because
122
+ * it never descends into an ignored directory; a flat path list does not, so
123
+ * callers staging untracked files must use this variant to match the watcher.
124
+ */
125
+ export function isRelPathIgnored(relPath, sets) {
126
+ const segments = relPath.split('/');
127
+ for (let i = 0; i < segments.length - 1; i++) {
128
+ if (sets.dirs.has(segments[i]))
129
+ return true;
130
+ }
131
+ return isPathIgnored(relPath, sets);
132
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runwork",
3
- "version": "0.13.3",
3
+ "version": "0.13.4",
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)",