@webpieces/pr-gate 0.4.565 → 0.4.567

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 (33) hide show
  1. package/bin/wp-finish-push-dev.js +27 -0
  2. package/bin/wp-push-dev.js +27 -0
  3. package/package.json +5 -3
  4. package/src/scripts/commands/finish-push-dev-command.d.ts +28 -0
  5. package/src/scripts/commands/finish-push-dev-command.js +62 -0
  6. package/src/scripts/commands/finish-push-dev-command.js.map +1 -0
  7. package/src/scripts/commands/push-dev-command.d.ts +91 -0
  8. package/src/scripts/commands/push-dev-command.js +256 -0
  9. package/src/scripts/commands/push-dev-command.js.map +1 -0
  10. package/src/scripts/pr-gate-app.d.ts +22 -1
  11. package/src/scripts/pr-gate-app.js +46 -2
  12. package/src/scripts/pr-gate-app.js.map +1 -1
  13. package/src/scripts/workflow/dev-deploy-refs.d.ts +74 -0
  14. package/src/scripts/workflow/dev-deploy-refs.js +153 -0
  15. package/src/scripts/workflow/dev-deploy-refs.js.map +1 -0
  16. package/src/scripts/workflow/dev-deploy-watch-hints.d.ts +35 -0
  17. package/src/scripts/workflow/dev-deploy-watch-hints.js +76 -0
  18. package/src/scripts/workflow/dev-deploy-watch-hints.js.map +1 -0
  19. package/src/scripts/workflow/dev-resolve-runner.d.ts +74 -0
  20. package/src/scripts/workflow/dev-resolve-runner.js +188 -0
  21. package/src/scripts/workflow/dev-resolve-runner.js.map +1 -0
  22. package/src/scripts/workflow/git-exec.d.ts +20 -1
  23. package/src/scripts/workflow/git-exec.js +30 -3
  24. package/src/scripts/workflow/git-exec.js.map +1 -1
  25. package/src/scripts/workflow/push-dev-state.d.ts +52 -0
  26. package/src/scripts/workflow/push-dev-state.js +147 -0
  27. package/src/scripts/workflow/push-dev-state.js.map +1 -0
  28. package/src/scripts/wp-finish-push-dev.d.ts +2 -0
  29. package/src/scripts/wp-finish-push-dev.js +20 -0
  30. package/src/scripts/wp-finish-push-dev.js.map +1 -0
  31. package/src/scripts/wp-push-dev.d.ts +2 -0
  32. package/src/scripts/wp-push-dev.js +44 -0
  33. package/src/scripts/wp-push-dev.js.map +1 -0
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DevResolveRunner = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const rules_config_1 = require("@webpieces/rules-config");
6
+ const inversify_1 = require("inversify");
7
+ const git_exec_1 = require("./git-exec");
8
+ const dev_deploy_refs_1 = require("./dev-deploy-refs");
9
+ const push_dev_state_1 = require("./push-dev-state");
10
+ const dev_deploy_watch_hints_1 = require("./dev-deploy-watch-hints");
11
+ const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
12
+ /**
13
+ * The queue-draining engine behind `wp-push-dev --resolve` and `wp-finish-push-dev`.
14
+ *
15
+ * ONE class owns every git WRITE in the resolve flow, and that is a hard requirement rather than tidiness:
16
+ * `redirect-how-to-merge-main` blocks `git merge` in every form INCLUDING `--continue`, and
17
+ * `pr-creation-or-push-guard` blocks every `git push`. So an AI cannot be told "now run `git commit`" or
18
+ * "`git merge --continue`" — those instructions are unrunnable by the only party reading them. The command
19
+ * owns the merges, the commit and the push (as child processes the hooks never see); the AI's entire job is
20
+ * editing the conflicted files and `git add`-ing them, neither of which is blocked.
21
+ */
22
+ let DevResolveRunner = class DevResolveRunner {
23
+ gitExec;
24
+ refs;
25
+ store;
26
+ watchHints;
27
+ constructor(gitExec, refs, store, watchHints) {
28
+ this.gitExec = gitExec;
29
+ this.refs = refs;
30
+ this.store = store;
31
+ this.watchHints = watchHints;
32
+ }
33
+ /**
34
+ * Start a resolve: park on a throwaway branch at the current copy, then drain the queue.
35
+ *
36
+ * The checkout is `-B <tmp> <base>` and NEVER the feature branch, which is the cardinal constraint made
37
+ * mechanical — every merge below lands on a branch that is deleted at the end, so no other developer's
38
+ * commits can reach the PR head even if the resolve is abandoned halfway.
39
+ *
40
+ * `base` differs between the two callers and the difference IS the semantics. `--resolve` starts at the
41
+ * published copy and merges the other copies in (compose what CI will build). `--rebase-resolution`
42
+ * starts at the LOCAL HEAD and merges the published copy in (replay an existing resolution on top of
43
+ * the new commits, instead of force-pushing over it).
44
+ *
45
+ * `fetchRefs` is passed separately from the queue because `--rebase-resolution` merges a ref that is
46
+ * also its push destination, so "everything to fetch" and "everything to merge" are not the same list.
47
+ */
48
+ start(repoRoot, state, base, fetchRefs) {
49
+ this.refs.fetchCopies(repoRoot, fetchRefs);
50
+ this.store.write(repoRoot, state);
51
+ this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', '-B', state.tmpBranch, base], `Failed to create the throwaway resolve branch ${state.tmpBranch}`);
52
+ this.drain(repoRoot, state);
53
+ }
54
+ /**
55
+ * Resume after the human/AI has resolved the halted merge: commit it, then keep draining.
56
+ *
57
+ * REFETCHES the remaining queue first. A resolution session can run long, and queuing against refs
58
+ * fetched an hour ago rehearses a composition CI will not build — the one freshness gap the sibling
59
+ * update flow is still known to have (only its stage ① ever fetches).
60
+ */
61
+ resume(repoRoot, state) {
62
+ this.assertNoUnmergedPaths(repoRoot, state);
63
+ this.gitExec.assertNoUntracked(repoRoot);
64
+ this.gitExec.runGitChecked(['-C', repoRoot, 'add', '-u'], 'Failed to stage the resolved files (git add -u)');
65
+ this.gitExec.runGitChecked(['-C', repoRoot, 'commit', '--no-edit'], `Failed to commit the resolution of ${state.current}`);
66
+ process.stdout.write(`✅ Committed the resolution of ${state.current}.\n`);
67
+ state.current = '';
68
+ this.refs.fetchCopies(repoRoot, state.queue);
69
+ this.drain(repoRoot, state);
70
+ }
71
+ /**
72
+ * Throw the whole resolve away: undo any halted merge, go back to the original branch, delete the
73
+ * throwaway branch and the state file. Nothing published, nothing on the feature branch.
74
+ */
75
+ abort(repoRoot, state) {
76
+ // Allowed to fail: there is no merge to abort when the resolve halted between merges, and that is
77
+ // not an error condition — the rest of the teardown must still run.
78
+ this.gitExec.tryGit(['-C', repoRoot, 'merge', '--abort'], repoRoot);
79
+ this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch], `Failed to return to ${state.originalBranch}`);
80
+ this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);
81
+ this.store.clear(repoRoot);
82
+ process.stdout.write('\n' + SEP + '🛑 Dev-deploy resolve aborted\n' + SEP + '\n'
83
+ + `You are back on \`${state.originalBranch}\`; \`${state.tmpBranch}\` and the state file are gone.\n`
84
+ + `The remote copy \`${state.targetRef}\` is untouched, and so is your feature branch.\n`);
85
+ }
86
+ /**
87
+ * Merge queued refs until one conflicts (HALT, hand the tree over) or the queue empties (PUBLISH).
88
+ *
89
+ * A LATER ref can conflict too, which is why this is a loop resumed from a state file rather than a
90
+ * single pass: `wp-finish-push-dev` can legitimately stop again on the next ref in the queue.
91
+ */
92
+ drain(repoRoot, state) {
93
+ while (state.queue.length > 0) {
94
+ const ref = state.queue[0];
95
+ state.queue = state.queue.slice(1);
96
+ const outcome = this.gitExec.tryGit(['-C', repoRoot, 'merge', '--no-ff', '--no-edit', '-m',
97
+ `Dev-deploy: merge ${ref} into ${state.targetRef}`, `refs/remotes/origin/${ref}`], repoRoot);
98
+ if (outcome.ok) {
99
+ process.stdout.write(` ✓ merged ${ref}\n`);
100
+ continue;
101
+ }
102
+ state.current = ref;
103
+ this.store.write(repoRoot, state);
104
+ this.halt(repoRoot, state);
105
+ return;
106
+ }
107
+ this.publish(repoRoot, state);
108
+ }
109
+ /** Stop with the conflicted files listed and the ONE command that continues. Exit 2 = "your turn". */
110
+ halt(repoRoot, state) {
111
+ const conflicted = this.unmergedPaths(repoRoot);
112
+ const remaining = state.queue.length > 0
113
+ ? `\nStill queued after this one: ${state.queue.join(', ')}\n`
114
+ + `\`${rules_config_1.WP_FINISH_PUSH_DEV}\` resumes the queue, so it may stop again on a later ref.\n`
115
+ : '';
116
+ throw new rules_config_1.CliExitError(2, '\n' + SEP + `⚠️ CONFLICT merging ${state.current}\n` + SEP + '\n'
117
+ + `You are on the throwaway branch \`${state.tmpBranch}\` — NOT \`${state.originalBranch}\`. Nothing here\n`
118
+ + 'can reach your feature branch or your PR, which is exactly why the resolution happens over here.\n\n'
119
+ + 'Resolve these files (edit until no conflict markers remain, then `git add` each one):\n'
120
+ + conflicted.map((f) => ` ${f}`).join('\n') + '\n'
121
+ + remaining
122
+ + '\nThen run ONE of:\n'
123
+ + ` ${rules_config_1.WP_FINISH_PUSH_DEV} ← commit, resume the queue, publish the copy\n`
124
+ + ` ${rules_config_1.WP_FINISH_PUSH_DEV} --abort ← throw it away and go back to \`${state.originalBranch}\`\n\n`
125
+ + 'Do NOT run `git merge --continue`, `git commit` or `git push` yourself — they are blocked, and\n'
126
+ + 'the finish command above does all three for you.\n');
127
+ }
128
+ /** Queue empty: force-push the composed result over the copy, then restore the original checkout. */
129
+ publish(repoRoot, state) {
130
+ this.gitExec.runGitChecked(['-C', repoRoot, 'push', '--force', 'origin', `${state.tmpBranch}:refs/heads/${state.targetRef}`], `Failed to publish the dev copy ${state.targetRef}`);
131
+ // Captured BEFORE the tmp branch is deleted — it is the composed sha the watch hints are about,
132
+ // and after the checkout below HEAD is the feature branch, which is a different commit entirely.
133
+ const published = this.refs.headSha(repoRoot, state.tmpBranch);
134
+ this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch], `Failed to return to ${state.originalBranch}`);
135
+ this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);
136
+ this.store.clear(repoRoot);
137
+ process.stdout.write('\n' + SEP + `✅ Published ${state.targetRef} with the resolutions\n` + SEP + '\n'
138
+ + `You are back on \`${state.originalBranch}\`, which was never modified — verify with:\n`
139
+ + ` git log --oneline ${state.originalBranch}\n`
140
+ + 'Your CI will pick the copy up on its next composition run.\n'
141
+ + this.watchHints.render(this.refs.config(repoRoot), state.targetRef, published));
142
+ }
143
+ /** Conflicted (unmerged-in-index) paths. Empty once every one has been resolved and `git add`ed. */
144
+ unmergedPaths(repoRoot) {
145
+ const out = this.gitExec.gitQuery(['-C', repoRoot, 'diff', '--name-only', '--diff-filter=U'], repoRoot, 'Failed to list the conflicted files (git diff --name-only --diff-filter=U).');
146
+ return out.split('\n').filter((l) => l.trim() !== '');
147
+ }
148
+ // The gate on `wp-finish-push-dev`: refuse (and re-print the list) while anything is still unmerged.
149
+ assertNoUnmergedPaths(repoRoot, state) {
150
+ const conflicted = this.unmergedPaths(repoRoot);
151
+ if (conflicted.length === 0)
152
+ return;
153
+ throw new rules_config_1.CliExitError(2, '\n' + SEP + '⛔ Conflicts are NOT resolved yet\n' + SEP + '\n'
154
+ + `${rules_config_1.WP_FINISH_PUSH_DEV} commits the resolution — it cannot write one for you. These files are still\n`
155
+ + 'unmerged (edit until no conflict markers remain, then `git add` each one):\n'
156
+ + conflicted.map((f) => ` ${f}`).join('\n') + '\n\n'
157
+ + `Merging: ${state.current === '' ? '(none)' : state.current}\n`
158
+ + `Give up instead with: ${rules_config_1.WP_FINISH_PUSH_DEV} --abort\n`);
159
+ }
160
+ /**
161
+ * The name of the throwaway branch for a resolve of `branch`.
162
+ *
163
+ * Suffixed rather than prefixed, matching the existing `<feature>Squash` / `<feature>PreMerge<n>`
164
+ * convention — and deliberately NOT inside the dev namespace, which holds REMOTE copies only.
165
+ */
166
+ tmpBranchFor(branch) {
167
+ return `${branch}DevResolve`;
168
+ }
169
+ /** The banner `--resolve` prints before the first merge, so the queue is visible before it halts. */
170
+ announce(cfg, state) {
171
+ return '\n' + SEP + `🔀 Resolving the dev composition for ${state.targetRef}\n` + SEP + '\n'
172
+ + `Merging ${String(state.queue.length)} other cop${state.queue.length === 1 ? 'y' : 'ies'} from `
173
+ + `\`${cfg.copyRefGlob()}\`, in the order your CI composes\n\`${cfg.devBranch}\`, onto the throwaway branch `
174
+ + `\`${state.tmpBranch}\`:\n`
175
+ + state.queue.map((r) => ` ${r}`).join('\n') + '\n\n'
176
+ + `Your feature branch \`${state.originalBranch}\` is not touched by any of this.\n`
177
+ + `A clean run publishes the copy itself — ${rules_config_1.WP_FINISH_PUSH_DEV} is only needed if a merge stops.\n\n`;
178
+ }
179
+ };
180
+ exports.DevResolveRunner = DevResolveRunner;
181
+ exports.DevResolveRunner = DevResolveRunner = tslib_1.__decorate([
182
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
183
+ tslib_1.__metadata("design:paramtypes", [git_exec_1.GitExec,
184
+ dev_deploy_refs_1.DevDeployRefs,
185
+ push_dev_state_1.PushDevStateStore,
186
+ dev_deploy_watch_hints_1.DevDeployWatchHints])
187
+ ], DevResolveRunner);
188
+ //# sourceMappingURL=dev-resolve-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-resolve-runner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/dev-resolve-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliExitError, DevDeployConfig, WP_FINISH_PUSH_DEV } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GitExec } from './git-exec';\nimport { DevDeployRefs } from './dev-deploy-refs';\nimport { PushDevState, PushDevStateStore } from './push-dev-state';\nimport { DevDeployWatchHints } from './dev-deploy-watch-hints';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The queue-draining engine behind `wp-push-dev --resolve` and `wp-finish-push-dev`.\n *\n * ONE class owns every git WRITE in the resolve flow, and that is a hard requirement rather than tidiness:\n * `redirect-how-to-merge-main` blocks `git merge` in every form INCLUDING `--continue`, and\n * `pr-creation-or-push-guard` blocks every `git push`. So an AI cannot be told \"now run `git commit`\" or\n * \"`git merge --continue`\" — those instructions are unrunnable by the only party reading them. The command\n * owns the merges, the commit and the push (as child processes the hooks never see); the AI's entire job is\n * editing the conflicted files and `git add`-ing them, neither of which is blocked.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DevResolveRunner {\n constructor(\n private readonly gitExec: GitExec,\n private readonly refs: DevDeployRefs,\n private readonly store: PushDevStateStore,\n private readonly watchHints: DevDeployWatchHints,\n ) {}\n\n /**\n * Start a resolve: park on a throwaway branch at the current copy, then drain the queue.\n *\n * The checkout is `-B <tmp> <base>` and NEVER the feature branch, which is the cardinal constraint made\n * mechanical — every merge below lands on a branch that is deleted at the end, so no other developer's\n * commits can reach the PR head even if the resolve is abandoned halfway.\n *\n * `base` differs between the two callers and the difference IS the semantics. `--resolve` starts at the\n * published copy and merges the other copies in (compose what CI will build). `--rebase-resolution`\n * starts at the LOCAL HEAD and merges the published copy in (replay an existing resolution on top of\n * the new commits, instead of force-pushing over it).\n *\n * `fetchRefs` is passed separately from the queue because `--rebase-resolution` merges a ref that is\n * also its push destination, so \"everything to fetch\" and \"everything to merge\" are not the same list.\n */\n start(repoRoot: string, state: PushDevState, base: string, fetchRefs: string[]): void {\n this.refs.fetchCopies(repoRoot, fetchRefs);\n this.store.write(repoRoot, state);\n this.gitExec.runGitChecked(\n ['-C', repoRoot, 'checkout', '-B', state.tmpBranch, base],\n `Failed to create the throwaway resolve branch ${state.tmpBranch}`);\n this.drain(repoRoot, state);\n }\n\n /**\n * Resume after the human/AI has resolved the halted merge: commit it, then keep draining.\n *\n * REFETCHES the remaining queue first. A resolution session can run long, and queuing against refs\n * fetched an hour ago rehearses a composition CI will not build — the one freshness gap the sibling\n * update flow is still known to have (only its stage ① ever fetches).\n */\n resume(repoRoot: string, state: PushDevState): void {\n this.assertNoUnmergedPaths(repoRoot, state);\n this.gitExec.assertNoUntracked(repoRoot);\n this.gitExec.runGitChecked(['-C', repoRoot, 'add', '-u'], 'Failed to stage the resolved files (git add -u)');\n this.gitExec.runGitChecked(['-C', repoRoot, 'commit', '--no-edit'],\n `Failed to commit the resolution of ${state.current}`);\n process.stdout.write(`✅ Committed the resolution of ${state.current}.\\n`);\n state.current = '';\n this.refs.fetchCopies(repoRoot, state.queue);\n this.drain(repoRoot, state);\n }\n\n /**\n * Throw the whole resolve away: undo any halted merge, go back to the original branch, delete the\n * throwaway branch and the state file. Nothing published, nothing on the feature branch.\n */\n abort(repoRoot: string, state: PushDevState): void {\n // Allowed to fail: there is no merge to abort when the resolve halted between merges, and that is\n // not an error condition — the rest of the teardown must still run.\n this.gitExec.tryGit(['-C', repoRoot, 'merge', '--abort'], repoRoot);\n this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch],\n `Failed to return to ${state.originalBranch}`);\n this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);\n this.store.clear(repoRoot);\n process.stdout.write(\n '\\n' + SEP + '🛑 Dev-deploy resolve aborted\\n' + SEP + '\\n'\n + `You are back on \\`${state.originalBranch}\\`; \\`${state.tmpBranch}\\` and the state file are gone.\\n`\n + `The remote copy \\`${state.targetRef}\\` is untouched, and so is your feature branch.\\n`);\n }\n\n /**\n * Merge queued refs until one conflicts (HALT, hand the tree over) or the queue empties (PUBLISH).\n *\n * A LATER ref can conflict too, which is why this is a loop resumed from a state file rather than a\n * single pass: `wp-finish-push-dev` can legitimately stop again on the next ref in the queue.\n */\n private drain(repoRoot: string, state: PushDevState): void {\n while (state.queue.length > 0) {\n const ref = state.queue[0];\n state.queue = state.queue.slice(1);\n const outcome = this.gitExec.tryGit(\n ['-C', repoRoot, 'merge', '--no-ff', '--no-edit', '-m',\n `Dev-deploy: merge ${ref} into ${state.targetRef}`, `refs/remotes/origin/${ref}`],\n repoRoot);\n if (outcome.ok) {\n process.stdout.write(` ✓ merged ${ref}\\n`);\n continue;\n }\n state.current = ref;\n this.store.write(repoRoot, state);\n this.halt(repoRoot, state);\n return;\n }\n this.publish(repoRoot, state);\n }\n\n /** Stop with the conflicted files listed and the ONE command that continues. Exit 2 = \"your turn\". */\n private halt(repoRoot: string, state: PushDevState): never {\n const conflicted = this.unmergedPaths(repoRoot);\n const remaining = state.queue.length > 0\n ? `\\nStill queued after this one: ${state.queue.join(', ')}\\n`\n + `\\`${WP_FINISH_PUSH_DEV}\\` resumes the queue, so it may stop again on a later ref.\\n`\n : '';\n throw new CliExitError(2,\n '\\n' + SEP + `⚠️ CONFLICT merging ${state.current}\\n` + SEP + '\\n'\n + `You are on the throwaway branch \\`${state.tmpBranch}\\` — NOT \\`${state.originalBranch}\\`. Nothing here\\n`\n + 'can reach your feature branch or your PR, which is exactly why the resolution happens over here.\\n\\n'\n + 'Resolve these files (edit until no conflict markers remain, then `git add` each one):\\n'\n + conflicted.map((f: string): string => ` ${f}`).join('\\n') + '\\n'\n + remaining\n + '\\nThen run ONE of:\\n'\n + ` ${WP_FINISH_PUSH_DEV} ← commit, resume the queue, publish the copy\\n`\n + ` ${WP_FINISH_PUSH_DEV} --abort ← throw it away and go back to \\`${state.originalBranch}\\`\\n\\n`\n + 'Do NOT run `git merge --continue`, `git commit` or `git push` yourself — they are blocked, and\\n'\n + 'the finish command above does all three for you.\\n');\n }\n\n /** Queue empty: force-push the composed result over the copy, then restore the original checkout. */\n private publish(repoRoot: string, state: PushDevState): void {\n this.gitExec.runGitChecked(\n ['-C', repoRoot, 'push', '--force', 'origin', `${state.tmpBranch}:refs/heads/${state.targetRef}`],\n `Failed to publish the dev copy ${state.targetRef}`);\n // Captured BEFORE the tmp branch is deleted — it is the composed sha the watch hints are about,\n // and after the checkout below HEAD is the feature branch, which is a different commit entirely.\n const published = this.refs.headSha(repoRoot, state.tmpBranch);\n this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch],\n `Failed to return to ${state.originalBranch}`);\n this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);\n this.store.clear(repoRoot);\n process.stdout.write(\n '\\n' + SEP + `✅ Published ${state.targetRef} with the resolutions\\n` + SEP + '\\n'\n + `You are back on \\`${state.originalBranch}\\`, which was never modified — verify with:\\n`\n + ` git log --oneline ${state.originalBranch}\\n`\n + 'Your CI will pick the copy up on its next composition run.\\n'\n + this.watchHints.render(this.refs.config(repoRoot), state.targetRef, published));\n }\n\n /** Conflicted (unmerged-in-index) paths. Empty once every one has been resolved and `git add`ed. */\n private unmergedPaths(repoRoot: string): string[] {\n const out = this.gitExec.gitQuery(['-C', repoRoot, 'diff', '--name-only', '--diff-filter=U'], repoRoot,\n 'Failed to list the conflicted files (git diff --name-only --diff-filter=U).');\n return out.split('\\n').filter((l: string): boolean => l.trim() !== '');\n }\n\n // The gate on `wp-finish-push-dev`: refuse (and re-print the list) while anything is still unmerged.\n private assertNoUnmergedPaths(repoRoot: string, state: PushDevState): void {\n const conflicted = this.unmergedPaths(repoRoot);\n if (conflicted.length === 0) return;\n throw new CliExitError(2,\n '\\n' + SEP + '⛔ Conflicts are NOT resolved yet\\n' + SEP + '\\n'\n + `${WP_FINISH_PUSH_DEV} commits the resolution — it cannot write one for you. These files are still\\n`\n + 'unmerged (edit until no conflict markers remain, then `git add` each one):\\n'\n + conflicted.map((f: string): string => ` ${f}`).join('\\n') + '\\n\\n'\n + `Merging: ${state.current === '' ? '(none)' : state.current}\\n`\n + `Give up instead with: ${WP_FINISH_PUSH_DEV} --abort\\n`);\n }\n\n /**\n * The name of the throwaway branch for a resolve of `branch`.\n *\n * Suffixed rather than prefixed, matching the existing `<feature>Squash` / `<feature>PreMerge<n>`\n * convention — and deliberately NOT inside the dev namespace, which holds REMOTE copies only.\n */\n tmpBranchFor(branch: string): string {\n return `${branch}DevResolve`;\n }\n\n /** The banner `--resolve` prints before the first merge, so the queue is visible before it halts. */\n announce(cfg: DevDeployConfig, state: PushDevState): string {\n return '\\n' + SEP + `🔀 Resolving the dev composition for ${state.targetRef}\\n` + SEP + '\\n'\n + `Merging ${String(state.queue.length)} other cop${state.queue.length === 1 ? 'y' : 'ies'} from `\n + `\\`${cfg.copyRefGlob()}\\`, in the order your CI composes\\n\\`${cfg.devBranch}\\`, onto the throwaway branch `\n + `\\`${state.tmpBranch}\\`:\\n`\n + state.queue.map((r: string): string => ` ${r}`).join('\\n') + '\\n\\n'\n + `Your feature branch \\`${state.originalBranch}\\` is not touched by any of this.\\n`\n + `A clean run publishes the copy itself — ${WP_FINISH_PUSH_DEV} is only needed if a merge stops.\\n\\n`;\n }\n}\n"]}
@@ -1,5 +1,16 @@
1
1
  import { RepoRootFinder } from '@webpieces/rules-config';
2
2
  import { GitStatusEntry, GitStatusParser } from './git-status';
3
+ /**
4
+ * The result of a git command that was allowed to fail. Data-only (per CLAUDE.md) so a caller asks
5
+ * `outcome.ok` rather than re-deriving "did it work" from a status number at every site.
6
+ */
7
+ export declare class GitOutcome {
8
+ status: number;
9
+ stdout: string;
10
+ stderr: string;
11
+ constructor(status: number, stdout: string, stderr: string);
12
+ get ok(): boolean;
13
+ }
3
14
  /** Shared git precondition checks + push logic for the PR/merge workflow. */
4
15
  export declare class GitExec {
5
16
  private readonly repoRootFinder;
@@ -48,6 +59,14 @@ export declare class GitExec {
48
59
  * 3-point squash rewrites history) and -u for a brand-new branch.
49
60
  */
50
61
  ensurePushed(currentBranch: string): void;
51
- private gitQuery;
62
+ /**
63
+ * Run a git command that MAY legitimately fail and report the outcome instead of aborting — for the
64
+ * questions whose "no" is an answer rather than an error (does this remote ref exist? did this merge
65
+ * conflict?). Distinct from {@link runGitChecked}, which exists for the commands where continuing on
66
+ * failure would operate on wrong state.
67
+ */
68
+ tryGit(args: string[], cwd: string): GitOutcome;
69
+ /** Read-only git query that MUST succeed, trimmed. Public sibling of the private raw form. */
70
+ gitQuery(args: string[], cwd: string, failMsg: string): string;
52
71
  private rawGitQuery;
53
72
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GitExec = void 0;
3
+ exports.GitExec = exports.GitOutcome = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const child_process_1 = require("child_process");
6
6
  const rules_config_1 = require("@webpieces/rules-config");
@@ -10,6 +10,24 @@ const SEP = '━━━━━━━━━━━━━━━━━━━━━━
10
10
  // The AI-facing doc the failure messages point at (generated under `.webpieces/instruct-ai`, at the
11
11
  // REPO ROOT — resolved absolute so the AI opens it regardless of the cwd it reads the message from).
12
12
  const GIT_WORKFLOW_DOC_NAME = 'webpieces.git-workflow.md';
13
+ /**
14
+ * The result of a git command that was allowed to fail. Data-only (per CLAUDE.md) so a caller asks
15
+ * `outcome.ok` rather than re-deriving "did it work" from a status number at every site.
16
+ */
17
+ class GitOutcome {
18
+ status;
19
+ stdout;
20
+ stderr;
21
+ constructor(status, stdout, stderr) {
22
+ this.status = status;
23
+ this.stdout = stdout;
24
+ this.stderr = stderr;
25
+ }
26
+ get ok() {
27
+ return this.status === 0;
28
+ }
29
+ }
30
+ exports.GitOutcome = GitOutcome;
13
31
  /** Shared git precondition checks + push logic for the PR/merge workflow. */
14
32
  let GitExec = class GitExec {
15
33
  repoRootFinder;
@@ -106,8 +124,17 @@ let GitExec = class GitExec {
106
124
  this.runGitChecked(['push', '-u', 'origin', `HEAD:${currentBranch}`], 'Failed to push new branch');
107
125
  }
108
126
  }
109
- // Run a read-only git query from the repo root; abort if git itself errors. Kept `cwd`-explicit
110
- // because `git ls-files --others` is scoped to the cwd subtree.
127
+ /**
128
+ * Run a git command that MAY legitimately fail and report the outcome instead of aborting — for the
129
+ * questions whose "no" is an answer rather than an error (does this remote ref exist? did this merge
130
+ * conflict?). Distinct from {@link runGitChecked}, which exists for the commands where continuing on
131
+ * failure would operate on wrong state.
132
+ */
133
+ tryGit(args, cwd) {
134
+ const out = (0, child_process_1.spawnSync)('git', args, { encoding: 'utf8', cwd });
135
+ return new GitOutcome(out.status ?? 1, (out.stdout ?? '').trim(), (out.stderr ?? '').trim());
136
+ }
137
+ /** Read-only git query that MUST succeed, trimmed. Public sibling of the private raw form. */
111
138
  gitQuery(args, cwd, failMsg) {
112
139
  return this.rawGitQuery(args, cwd, failMsg).trim();
113
140
  }
@@ -1 +1 @@
1
- {"version":3,"file":"git-exec.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-exec.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { CliExitError, RepoRootFinder } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { GitStatusEntry, GitStatusParser } from './git-status';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n// The AI-facing doc the failure messages point at (generated under `.webpieces/instruct-ai`, at the\n// REPO ROOT — resolved absolute so the AI opens it regardless of the cwd it reads the message from).\nconst GIT_WORKFLOW_DOC_NAME = 'webpieces.git-workflow.md';\n\n/** Shared git precondition checks + push logic for the PR/merge workflow. */\n@injectable(bindingScopeValues.Singleton)\nexport class GitExec {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly statusParser: GitStatusParser,\n ) {}\n\n /**\n * Raw `git status --porcelain` with ONLY the trailing newline(s) stripped — never `.trim()`.\n *\n * There used to be a trimmed sibling here (`uncommittedFiles`) and it was a data-destroying trap:\n * porcelain column 1 is the index state and column 2 the worktree state, so an unstaged\n * modification is \" M path\"; `trim()` eats that leading space and the line then reads \"M path\",\n * i.e. STAGED — the exact inverse of the truth. It is deleted. This method is for EMPTINESS\n * (\"is the tree dirty at all?\") and for PRINTING; anything that reasons about the two columns\n * calls {@link statusEntries} and reads booleans instead of re-parsing a string.\n *\n * Tracked changes (staged + unstaged) AND untracked files, EXCLUDING gitignored paths — so\n * `.webpieces/` tooling artifacts never count. Empty string = fully committed.\n */\n porcelainStatus(cwd: string): string {\n return this.rawGitQuery(['status', '--porcelain'], cwd,\n 'Failed to run `git status --porcelain` to check the working tree.').replace(/\\n+$/, '');\n }\n\n /**\n * The same status, PARSED: two status columns, an unquoted path, and explicit\n * `isStaged()`/`isUnstaged()`/`isUntracked()` answers. Use this for every semantic question.\n */\n statusEntries(cwd: string): GitStatusEntry[] {\n return this.statusParser.parse(this.porcelainStatus(cwd));\n }\n\n // Untracked files only (respects .gitignore). Empty string = none.\n untrackedFiles(cwd: string): string {\n return this.gitQuery(['ls-files', '--others', '--exclude-standard'], cwd, 'Failed to list untracked files (git ls-files --others).');\n }\n\n /**\n * Precondition for every PR/merge entry point: the working tree must be fully committed. The\n * webpieces tooling deliberately does NOT `git add`/`commit` the developer's work for them. Aborts\n * with instructions if anything is uncommitted (tracked or untracked, excluding gitignored).\n */\n assertCleanTree(cwd: string): void {\n const dirty = this.porcelainStatus(cwd);\n if (dirty === '') return;\n throw new CliExitError(1,\n '\\n' + SEP +\n '❌ ERROR: You have uncommitted or untracked changes\\n' +\n SEP + '\\n' +\n 'The webpieces PR tooling will NOT commit your work for you. Commit your\\n' +\n 'changes, and either commit or delete any untracked files, then re-run.\\n\\n' +\n 'Working tree (git status --porcelain):\\n' +\n dirty + '\\n\\n' +\n `See ${this.repoRootFinder.docPathFrom(cwd, GIT_WORKFLOW_DOC_NAME)} for the full merge + PR process.\\n` +\n SEP,\n );\n }\n\n /**\n * Narrower guard for the merge-resolve commit point, where tracked resolutions are legitimately in\n * the tree but untracked files must NOT be swept into the squash commit. Lists any untracked files\n * and aborts so the AI commits or deletes them explicitly.\n */\n assertNoUntracked(cwd: string): void {\n const untracked = this.untrackedFiles(cwd);\n if (untracked === '') return;\n throw new CliExitError(1,\n '\\n' + SEP +\n '❌ ERROR: Untracked files present during merge finalize\\n' +\n SEP + '\\n' +\n 'The tooling will not sweep untracked files into the squash commit. Commit\\n' +\n 'or delete these, then re-run:\\n\\n' +\n untracked + '\\n\\n' +\n `See ${this.repoRootFinder.docPathFrom(cwd, GIT_WORKFLOW_DOC_NAME)} for the full merge + PR process.\\n` +\n SEP,\n );\n }\n\n /**\n * Run a git command that is expected to succeed; abort the process with a clear message if it\n * fails. Used for fetch/pull/checkout/commit where silently continuing on failure would operate on\n * stale or wrong state.\n */\n runGitChecked(args: string[], errMsg: string): void {\n const result = spawnSync('git', args, { stdio: 'inherit' });\n if (result.status !== 0) {\n throw new CliExitError(1, `❌ ${errMsg} (git ${args.join(' ')} exited ${String(result.status)})`);\n }\n }\n\n /**\n * Push HEAD to origin/<currentBranch>. Uses --force-with-lease for an existing remote branch (the\n * 3-point squash rewrites history) and -u for a brand-new branch.\n */\n ensurePushed(currentBranch: string): void {\n const remoteExists = spawnSync('git', ['ls-remote', '--exit-code', '--heads', 'origin', currentBranch]).status === 0;\n if (remoteExists) {\n this.runGitChecked(['push', '--force-with-lease', 'origin', `HEAD:${currentBranch}`], 'Failed to push branch');\n } else {\n this.runGitChecked(['push', '-u', 'origin', `HEAD:${currentBranch}`], 'Failed to push new branch');\n }\n }\n\n // Run a read-only git query from the repo root; abort if git itself errors. Kept `cwd`-explicit\n // because `git ls-files --others` is scoped to the cwd subtree.\n private gitQuery(args: string[], cwd: string, failMsg: string): string {\n return this.rawGitQuery(args, cwd, failMsg).trim();\n }\n\n // The untrimmed sibling — see porcelainStatus for why trimming is not always safe.\n private rawGitQuery(args: string[], cwd: string, failMsg: string): string {\n const out = spawnSync('git', args, { encoding: 'utf8', cwd });\n if (out.status !== 0) {\n throw new CliExitError(1, `❌ ${failMsg}`);\n }\n return out.stdout;\n }\n}\n"]}
1
+ {"version":3,"file":"git-exec.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-exec.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { CliExitError, RepoRootFinder } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { GitStatusEntry, GitStatusParser } from './git-status';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n// The AI-facing doc the failure messages point at (generated under `.webpieces/instruct-ai`, at the\n// REPO ROOT — resolved absolute so the AI opens it regardless of the cwd it reads the message from).\nconst GIT_WORKFLOW_DOC_NAME = 'webpieces.git-workflow.md';\n\n/**\n * The result of a git command that was allowed to fail. Data-only (per CLAUDE.md) so a caller asks\n * `outcome.ok` rather than re-deriving \"did it work\" from a status number at every site.\n */\nexport class GitOutcome {\n status: number;\n stdout: string;\n stderr: string;\n\n constructor(status: number, stdout: string, stderr: string) {\n this.status = status;\n this.stdout = stdout;\n this.stderr = stderr;\n }\n\n get ok(): boolean {\n return this.status === 0;\n }\n}\n\n/** Shared git precondition checks + push logic for the PR/merge workflow. */\n@injectable(bindingScopeValues.Singleton)\nexport class GitExec {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly statusParser: GitStatusParser,\n ) {}\n\n /**\n * Raw `git status --porcelain` with ONLY the trailing newline(s) stripped — never `.trim()`.\n *\n * There used to be a trimmed sibling here (`uncommittedFiles`) and it was a data-destroying trap:\n * porcelain column 1 is the index state and column 2 the worktree state, so an unstaged\n * modification is \" M path\"; `trim()` eats that leading space and the line then reads \"M path\",\n * i.e. STAGED — the exact inverse of the truth. It is deleted. This method is for EMPTINESS\n * (\"is the tree dirty at all?\") and for PRINTING; anything that reasons about the two columns\n * calls {@link statusEntries} and reads booleans instead of re-parsing a string.\n *\n * Tracked changes (staged + unstaged) AND untracked files, EXCLUDING gitignored paths — so\n * `.webpieces/` tooling artifacts never count. Empty string = fully committed.\n */\n porcelainStatus(cwd: string): string {\n return this.rawGitQuery(['status', '--porcelain'], cwd,\n 'Failed to run `git status --porcelain` to check the working tree.').replace(/\\n+$/, '');\n }\n\n /**\n * The same status, PARSED: two status columns, an unquoted path, and explicit\n * `isStaged()`/`isUnstaged()`/`isUntracked()` answers. Use this for every semantic question.\n */\n statusEntries(cwd: string): GitStatusEntry[] {\n return this.statusParser.parse(this.porcelainStatus(cwd));\n }\n\n // Untracked files only (respects .gitignore). Empty string = none.\n untrackedFiles(cwd: string): string {\n return this.gitQuery(['ls-files', '--others', '--exclude-standard'], cwd, 'Failed to list untracked files (git ls-files --others).');\n }\n\n /**\n * Precondition for every PR/merge entry point: the working tree must be fully committed. The\n * webpieces tooling deliberately does NOT `git add`/`commit` the developer's work for them. Aborts\n * with instructions if anything is uncommitted (tracked or untracked, excluding gitignored).\n */\n assertCleanTree(cwd: string): void {\n const dirty = this.porcelainStatus(cwd);\n if (dirty === '') return;\n throw new CliExitError(1,\n '\\n' + SEP +\n '❌ ERROR: You have uncommitted or untracked changes\\n' +\n SEP + '\\n' +\n 'The webpieces PR tooling will NOT commit your work for you. Commit your\\n' +\n 'changes, and either commit or delete any untracked files, then re-run.\\n\\n' +\n 'Working tree (git status --porcelain):\\n' +\n dirty + '\\n\\n' +\n `See ${this.repoRootFinder.docPathFrom(cwd, GIT_WORKFLOW_DOC_NAME)} for the full merge + PR process.\\n` +\n SEP,\n );\n }\n\n /**\n * Narrower guard for the merge-resolve commit point, where tracked resolutions are legitimately in\n * the tree but untracked files must NOT be swept into the squash commit. Lists any untracked files\n * and aborts so the AI commits or deletes them explicitly.\n */\n assertNoUntracked(cwd: string): void {\n const untracked = this.untrackedFiles(cwd);\n if (untracked === '') return;\n throw new CliExitError(1,\n '\\n' + SEP +\n '❌ ERROR: Untracked files present during merge finalize\\n' +\n SEP + '\\n' +\n 'The tooling will not sweep untracked files into the squash commit. Commit\\n' +\n 'or delete these, then re-run:\\n\\n' +\n untracked + '\\n\\n' +\n `See ${this.repoRootFinder.docPathFrom(cwd, GIT_WORKFLOW_DOC_NAME)} for the full merge + PR process.\\n` +\n SEP,\n );\n }\n\n /**\n * Run a git command that is expected to succeed; abort the process with a clear message if it\n * fails. Used for fetch/pull/checkout/commit where silently continuing on failure would operate on\n * stale or wrong state.\n */\n runGitChecked(args: string[], errMsg: string): void {\n const result = spawnSync('git', args, { stdio: 'inherit' });\n if (result.status !== 0) {\n throw new CliExitError(1, `❌ ${errMsg} (git ${args.join(' ')} exited ${String(result.status)})`);\n }\n }\n\n /**\n * Push HEAD to origin/<currentBranch>. Uses --force-with-lease for an existing remote branch (the\n * 3-point squash rewrites history) and -u for a brand-new branch.\n */\n ensurePushed(currentBranch: string): void {\n const remoteExists = spawnSync('git', ['ls-remote', '--exit-code', '--heads', 'origin', currentBranch]).status === 0;\n if (remoteExists) {\n this.runGitChecked(['push', '--force-with-lease', 'origin', `HEAD:${currentBranch}`], 'Failed to push branch');\n } else {\n this.runGitChecked(['push', '-u', 'origin', `HEAD:${currentBranch}`], 'Failed to push new branch');\n }\n }\n\n /**\n * Run a git command that MAY legitimately fail and report the outcome instead of aborting — for the\n * questions whose \"no\" is an answer rather than an error (does this remote ref exist? did this merge\n * conflict?). Distinct from {@link runGitChecked}, which exists for the commands where continuing on\n * failure would operate on wrong state.\n */\n tryGit(args: string[], cwd: string): GitOutcome {\n const out = spawnSync('git', args, { encoding: 'utf8', cwd });\n return new GitOutcome(out.status ?? 1, (out.stdout ?? '').trim(), (out.stderr ?? '').trim());\n }\n\n /** Read-only git query that MUST succeed, trimmed. Public sibling of the private raw form. */\n gitQuery(args: string[], cwd: string, failMsg: string): string {\n return this.rawGitQuery(args, cwd, failMsg).trim();\n }\n\n // The untrimmed sibling — see porcelainStatus for why trimming is not always safe.\n private rawGitQuery(args: string[], cwd: string, failMsg: string): string {\n const out = spawnSync('git', args, { encoding: 'utf8', cwd });\n if (out.status !== 0) {\n throw new CliExitError(1, `❌ ${failMsg}`);\n }\n return out.stdout;\n }\n}\n"]}
@@ -0,0 +1,52 @@
1
+ import { DotWebpieces } from '@webpieces/rules-config';
2
+ /**
3
+ * The persisted half of the `wp-push-dev --resolve` state machine.
4
+ *
5
+ * A single-shot wrapper is IMPOSSIBLE here and that is why this file exists: `git merge` halts on
6
+ * conflict with markers in the working tree and a non-zero exit, and a human or an AI has to edit files
7
+ * before anything can be committed. So the command must be able to stop, hand the tree over, and be
8
+ * resumed by a second command that knows exactly where it left off.
9
+ *
10
+ * Data-only (per CLAUDE.md) — the behaviour lives in {@link PushDevStateStore}.
11
+ */
12
+ export declare class PushDevState {
13
+ /** The branch to return to when the resolve finishes or aborts. NEVER written to by this flow. */
14
+ originalBranch: string;
15
+ /** The local throwaway branch the merges happen on. Deleted on finish and on abort. */
16
+ tmpBranch: string;
17
+ /** `<namespace>/<originalBranch>` — the remote copy this resolve republishes. */
18
+ targetRef: string;
19
+ /** The copy refs still to merge, in the order CI composes them. */
20
+ queue: string[];
21
+ /** The ref currently mid-merge (conflicted), or '' when nothing is halted. */
22
+ current: string;
23
+ constructor(originalBranch: string, tmpBranch: string, targetRef: string, queue: string[], current?: string);
24
+ }
25
+ /**
26
+ * Reads, writes and clears the resolve state file, and refuses the other `wp-*` commands while it
27
+ * exists.
28
+ *
29
+ * LOCAL scope, deliberately: a resolve halts one worktree's checkout on a throwaway branch, and another
30
+ * worktree has no reason to care. Sharing it would block a colleague's PR flow on state that says
31
+ * nothing about their tree.
32
+ */
33
+ export declare class PushDevStateStore {
34
+ private readonly dotWebpieces;
35
+ constructor(dotWebpieces: DotWebpieces);
36
+ path(repoRoot: string): string;
37
+ exists(repoRoot: string): boolean;
38
+ /** The in-flight resolve, or null when none is. A corrupt file reads as null rather than crashing. */
39
+ read(repoRoot: string): PushDevState | null;
40
+ write(repoRoot: string, state: PushDevState): void;
41
+ clear(repoRoot: string): void;
42
+ /** The read that MUST find a resolve — used by `wp-finish-push-dev`, whose whole job is resuming one. */
43
+ require(repoRoot: string): PushDevState;
44
+ /**
45
+ * Refuse `attempted` while a resolve is half-finished. Called from the app root for every command in
46
+ * {@link BLOCKED_DURING_RESOLVE}, so no individual command has to remember to ask.
47
+ */
48
+ assertIdle(repoRoot: string, attempted: string): void;
49
+ private blockedCommandList;
50
+ /** Is `command` one of the ones that must refuse mid-resolve? Used by the app root. */
51
+ isBlockedDuringResolve(command: string): boolean;
52
+ }
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PushDevStateStore = exports.PushDevState = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const rules_config_1 = require("@webpieces/rules-config");
7
+ const inversify_1 = require("inversify");
8
+ const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
9
+ /**
10
+ * The persisted half of the `wp-push-dev --resolve` state machine.
11
+ *
12
+ * A single-shot wrapper is IMPOSSIBLE here and that is why this file exists: `git merge` halts on
13
+ * conflict with markers in the working tree and a non-zero exit, and a human or an AI has to edit files
14
+ * before anything can be committed. So the command must be able to stop, hand the tree over, and be
15
+ * resumed by a second command that knows exactly where it left off.
16
+ *
17
+ * Data-only (per CLAUDE.md) — the behaviour lives in {@link PushDevStateStore}.
18
+ */
19
+ class PushDevState {
20
+ /** The branch to return to when the resolve finishes or aborts. NEVER written to by this flow. */
21
+ originalBranch;
22
+ /** The local throwaway branch the merges happen on. Deleted on finish and on abort. */
23
+ tmpBranch;
24
+ /** `<namespace>/<originalBranch>` — the remote copy this resolve republishes. */
25
+ targetRef;
26
+ /** The copy refs still to merge, in the order CI composes them. */
27
+ queue;
28
+ /** The ref currently mid-merge (conflicted), or '' when nothing is halted. */
29
+ current;
30
+ // eslint-disable-next-line @typescript-eslint/max-params
31
+ constructor(originalBranch, tmpBranch, targetRef, queue, current = '') {
32
+ this.originalBranch = originalBranch;
33
+ this.tmpBranch = tmpBranch;
34
+ this.targetRef = targetRef;
35
+ this.queue = queue;
36
+ this.current = current;
37
+ }
38
+ }
39
+ exports.PushDevState = PushDevState;
40
+ // The wp-* commands that must REFUSE while a resolve is half-finished, and the ONE place that list is
41
+ // written. The blocked-list sentence below RENDERS ITSELF from this array — the same lesson
42
+ // merge-in-progress-guard already learned the hard way: a hand-written "don't run other commands"
43
+ // survives every edit to the actual enforcement and ends up forbidding things the flow requires.
44
+ //
45
+ // Why these and not everything: each one either rewrites the current branch (the two starts), pushes it
46
+ // (the finishes), or deletes branches (cleanup) — and during a resolve the checkout is a throwaway
47
+ // branch, not the feature branch any of them think they are acting on.
48
+ const BLOCKED_DURING_RESOLVE = [
49
+ 'wp-start-update', 'wp-finish-update',
50
+ 'wp-start-upsert-pr', 'wp-review-upsert-pr', 'wp-finish-upsert-pr',
51
+ 'wp-land-pr', 'wp-cleanup',
52
+ ];
53
+ /**
54
+ * Reads, writes and clears the resolve state file, and refuses the other `wp-*` commands while it
55
+ * exists.
56
+ *
57
+ * LOCAL scope, deliberately: a resolve halts one worktree's checkout on a throwaway branch, and another
58
+ * worktree has no reason to care. Sharing it would block a colleague's PR flow on state that says
59
+ * nothing about their tree.
60
+ */
61
+ let PushDevStateStore = class PushDevStateStore {
62
+ dotWebpieces;
63
+ constructor(dotWebpieces) {
64
+ this.dotWebpieces = dotWebpieces;
65
+ }
66
+ path(repoRoot) {
67
+ return this.dotWebpieces.localFile(repoRoot, rules_config_1.PUSH_DEV_STATE_FILE);
68
+ }
69
+ exists(repoRoot) {
70
+ return fs.existsSync(this.path(repoRoot));
71
+ }
72
+ /** The in-flight resolve, or null when none is. A corrupt file reads as null rather than crashing. */
73
+ read(repoRoot) {
74
+ const file = this.path(repoRoot);
75
+ if (!fs.existsSync(file))
76
+ return null;
77
+ // webpieces-disable no-unmanaged-exceptions -- a truncated/hand-edited state file must degrade to "no resolve in progress" (recoverable via --abort or a fresh --resolve), never crash every wp-* command in the tree
78
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
79
+ try {
80
+ // webpieces-disable no-any-unknown -- JSON.parse is untyped until narrowed on the next line
81
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
82
+ if (typeof raw.originalBranch !== 'string' || typeof raw.tmpBranch !== 'string')
83
+ return null;
84
+ return new PushDevState(raw.originalBranch, raw.tmpBranch, raw.targetRef ?? '', Array.isArray(raw.queue) ? raw.queue : [], raw.current ?? '');
85
+ // The parse error carries nothing a caller could act on: the ONLY meaningful reading of an
86
+ // unparseable state file is "no resolve in progress", which --abort and a fresh --resolve both
87
+ // recover from.
88
+ }
89
+ catch (err) {
90
+ const error = (0, rules_config_1.toError)(err);
91
+ void error;
92
+ return null;
93
+ }
94
+ }
95
+ write(repoRoot, state) {
96
+ fs.mkdirSync(this.dotWebpieces.local(repoRoot), { recursive: true });
97
+ fs.writeFileSync(this.path(repoRoot), JSON.stringify(state, null, 2) + '\n', 'utf8');
98
+ }
99
+ clear(repoRoot) {
100
+ const file = this.path(repoRoot);
101
+ if (fs.existsSync(file))
102
+ fs.rmSync(file);
103
+ }
104
+ /** The read that MUST find a resolve — used by `wp-finish-push-dev`, whose whole job is resuming one. */
105
+ require(repoRoot) {
106
+ const state = this.read(repoRoot);
107
+ if (state !== null)
108
+ return state;
109
+ throw new rules_config_1.CliExitError(2, '\n' + SEP + '⛔ No dev-deploy resolve is in progress\n' + SEP + '\n'
110
+ + `${rules_config_1.WP_FINISH_PUSH_DEV} resumes a resolve that \`${rules_config_1.WP_PUSH_DEV} --resolve\` started, and there is no\n`
111
+ + `state file at:\n ${this.path(repoRoot)}\n\n`
112
+ + 'If you meant to publish your branch to the shared dev server, that is the one-command form:\n'
113
+ + ` ${rules_config_1.WP_PUSH_DEV}\n`);
114
+ }
115
+ /**
116
+ * Refuse `attempted` while a resolve is half-finished. Called from the app root for every command in
117
+ * {@link BLOCKED_DURING_RESOLVE}, so no individual command has to remember to ask.
118
+ */
119
+ assertIdle(repoRoot, attempted) {
120
+ const state = this.read(repoRoot);
121
+ if (state === null)
122
+ return;
123
+ throw new rules_config_1.CliExitError(2, '\n' + SEP + `⛔ A dev-deploy resolve is in progress — \`pnpm ${attempted}\` is blocked\n` + SEP + '\n'
124
+ + `You are standing on the throwaway branch \`${state.tmpBranch}\`, not on \`${state.originalBranch}\`,\n`
125
+ + `so ${this.blockedCommandList()} would all act on the wrong branch.\n`
126
+ + 'That is the whole list; nothing else is blocked.\n\n'
127
+ + 'EXPECTED of you right now, and NOT blocked: read the conflicted files and edit them until every\n'
128
+ + 'conflict marker is gone. Then finish or bail out:\n'
129
+ + ` ${rules_config_1.WP_FINISH_PUSH_DEV} ← commit the resolution, resume the queue, publish the copy\n`
130
+ + ` ${rules_config_1.WP_FINISH_PUSH_DEV} --abort ← throw the resolution away and go back to \`${state.originalBranch}\`\n\n`
131
+ + `State file: ${this.path(repoRoot)}\n`);
132
+ }
133
+ // Rendered from BLOCKED_DURING_RESOLVE, never hand-written — see that constant.
134
+ blockedCommandList() {
135
+ return BLOCKED_DURING_RESOLVE.map((c) => `\`pnpm ${c}\``).join(', ');
136
+ }
137
+ /** Is `command` one of the ones that must refuse mid-resolve? Used by the app root. */
138
+ isBlockedDuringResolve(command) {
139
+ return BLOCKED_DURING_RESOLVE.includes(command);
140
+ }
141
+ };
142
+ exports.PushDevStateStore = PushDevStateStore;
143
+ exports.PushDevStateStore = PushDevStateStore = tslib_1.__decorate([
144
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
145
+ tslib_1.__metadata("design:paramtypes", [rules_config_1.DotWebpieces])
146
+ ], PushDevStateStore);
147
+ //# sourceMappingURL=push-dev-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-dev-state.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/push-dev-state.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\n\nimport {\n CliExitError,\n DotWebpieces,\n PUSH_DEV_STATE_FILE,\n WP_FINISH_PUSH_DEV,\n WP_PUSH_DEV,\n toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The persisted half of the `wp-push-dev --resolve` state machine.\n *\n * A single-shot wrapper is IMPOSSIBLE here and that is why this file exists: `git merge` halts on\n * conflict with markers in the working tree and a non-zero exit, and a human or an AI has to edit files\n * before anything can be committed. So the command must be able to stop, hand the tree over, and be\n * resumed by a second command that knows exactly where it left off.\n *\n * Data-only (per CLAUDE.md) — the behaviour lives in {@link PushDevStateStore}.\n */\nexport class PushDevState {\n /** The branch to return to when the resolve finishes or aborts. NEVER written to by this flow. */\n originalBranch: string;\n /** The local throwaway branch the merges happen on. Deleted on finish and on abort. */\n tmpBranch: string;\n /** `<namespace>/<originalBranch>` — the remote copy this resolve republishes. */\n targetRef: string;\n /** The copy refs still to merge, in the order CI composes them. */\n queue: string[];\n /** The ref currently mid-merge (conflicted), or '' when nothing is halted. */\n current: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(originalBranch: string, tmpBranch: string, targetRef: string, queue: string[], current = '') {\n this.originalBranch = originalBranch;\n this.tmpBranch = tmpBranch;\n this.targetRef = targetRef;\n this.queue = queue;\n this.current = current;\n }\n}\n\n// The wp-* commands that must REFUSE while a resolve is half-finished, and the ONE place that list is\n// written. The blocked-list sentence below RENDERS ITSELF from this array — the same lesson\n// merge-in-progress-guard already learned the hard way: a hand-written \"don't run other commands\"\n// survives every edit to the actual enforcement and ends up forbidding things the flow requires.\n//\n// Why these and not everything: each one either rewrites the current branch (the two starts), pushes it\n// (the finishes), or deletes branches (cleanup) — and during a resolve the checkout is a throwaway\n// branch, not the feature branch any of them think they are acting on.\nconst BLOCKED_DURING_RESOLVE: readonly string[] = [\n 'wp-start-update', 'wp-finish-update',\n 'wp-start-upsert-pr', 'wp-review-upsert-pr', 'wp-finish-upsert-pr',\n 'wp-land-pr', 'wp-cleanup',\n];\n\n/**\n * Reads, writes and clears the resolve state file, and refuses the other `wp-*` commands while it\n * exists.\n *\n * LOCAL scope, deliberately: a resolve halts one worktree's checkout on a throwaway branch, and another\n * worktree has no reason to care. Sharing it would block a colleague's PR flow on state that says\n * nothing about their tree.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class PushDevStateStore {\n constructor(private readonly dotWebpieces: DotWebpieces) {}\n\n path(repoRoot: string): string {\n return this.dotWebpieces.localFile(repoRoot, PUSH_DEV_STATE_FILE);\n }\n\n exists(repoRoot: string): boolean {\n return fs.existsSync(this.path(repoRoot));\n }\n\n /** The in-flight resolve, or null when none is. A corrupt file reads as null rather than crashing. */\n read(repoRoot: string): PushDevState | null {\n const file = this.path(repoRoot);\n if (!fs.existsSync(file)) return null;\n // webpieces-disable no-unmanaged-exceptions -- a truncated/hand-edited state file must degrade to \"no resolve in progress\" (recoverable via --abort or a fresh --resolve), never crash every wp-* command in the tree\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- JSON.parse is untyped until narrowed on the next line\n const raw = JSON.parse(fs.readFileSync(file, 'utf8')) as Partial<PushDevState>;\n if (typeof raw.originalBranch !== 'string' || typeof raw.tmpBranch !== 'string') return null;\n return new PushDevState(\n raw.originalBranch, raw.tmpBranch, raw.targetRef ?? '',\n Array.isArray(raw.queue) ? raw.queue : [], raw.current ?? '');\n // The parse error carries nothing a caller could act on: the ONLY meaningful reading of an\n // unparseable state file is \"no resolve in progress\", which --abort and a fresh --resolve both\n // recover from.\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n write(repoRoot: string, state: PushDevState): void {\n fs.mkdirSync(this.dotWebpieces.local(repoRoot), { recursive: true });\n fs.writeFileSync(this.path(repoRoot), JSON.stringify(state, null, 2) + '\\n', 'utf8');\n }\n\n clear(repoRoot: string): void {\n const file = this.path(repoRoot);\n if (fs.existsSync(file)) fs.rmSync(file);\n }\n\n /** The read that MUST find a resolve — used by `wp-finish-push-dev`, whose whole job is resuming one. */\n require(repoRoot: string): PushDevState {\n const state = this.read(repoRoot);\n if (state !== null) return state;\n throw new CliExitError(2,\n '\\n' + SEP + '⛔ No dev-deploy resolve is in progress\\n' + SEP + '\\n'\n + `${WP_FINISH_PUSH_DEV} resumes a resolve that \\`${WP_PUSH_DEV} --resolve\\` started, and there is no\\n`\n + `state file at:\\n ${this.path(repoRoot)}\\n\\n`\n + 'If you meant to publish your branch to the shared dev server, that is the one-command form:\\n'\n + ` ${WP_PUSH_DEV}\\n`);\n }\n\n /**\n * Refuse `attempted` while a resolve is half-finished. Called from the app root for every command in\n * {@link BLOCKED_DURING_RESOLVE}, so no individual command has to remember to ask.\n */\n assertIdle(repoRoot: string, attempted: string): void {\n const state = this.read(repoRoot);\n if (state === null) return;\n throw new CliExitError(2,\n '\\n' + SEP + `⛔ A dev-deploy resolve is in progress — \\`pnpm ${attempted}\\` is blocked\\n` + SEP + '\\n'\n + `You are standing on the throwaway branch \\`${state.tmpBranch}\\`, not on \\`${state.originalBranch}\\`,\\n`\n + `so ${this.blockedCommandList()} would all act on the wrong branch.\\n`\n + 'That is the whole list; nothing else is blocked.\\n\\n'\n + 'EXPECTED of you right now, and NOT blocked: read the conflicted files and edit them until every\\n'\n + 'conflict marker is gone. Then finish or bail out:\\n'\n + ` ${WP_FINISH_PUSH_DEV} ← commit the resolution, resume the queue, publish the copy\\n`\n + ` ${WP_FINISH_PUSH_DEV} --abort ← throw the resolution away and go back to \\`${state.originalBranch}\\`\\n\\n`\n + `State file: ${this.path(repoRoot)}\\n`);\n }\n\n // Rendered from BLOCKED_DURING_RESOLVE, never hand-written — see that constant.\n private blockedCommandList(): string {\n return BLOCKED_DURING_RESOLVE.map((c: string): string => `\\`pnpm ${c}\\``).join(', ');\n }\n\n /** Is `command` one of the ones that must refuse mid-resolve? Used by the app root. */\n isBlockedDuringResolve(command: string): boolean {\n return BLOCKED_DURING_RESOLVE.includes(command);\n }\n}\n"]}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import 'reflect-metadata';
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ require("reflect-metadata");
5
+ const inversify_1 = require("inversify");
6
+ const rules_config_1 = require("@webpieces/rules-config");
7
+ const pr_gate_app_1 = require("./pr-gate-app");
8
+ const finish_push_dev_command_1 = require("./commands/finish-push-dev-command");
9
+ const ABORT = '--abort';
10
+ // Composition root: build the container and resolve the app so inversify constructs the whole DAG.
11
+ (0, rules_config_1.runMain)(async () => {
12
+ // autobind self-binds every @injectable(Singleton) tooling class (replaces the buildProviderModule registry scan)
13
+ const container = new inversify_1.Container({ autobind: true });
14
+ // Reject `--help`/bogus flags BEFORE the app touches git — a mistyped `--abrt` that was silently
15
+ // ignored would COMMIT and PUBLISH the composition the caller was trying to throw away.
16
+ const args = container.get(rules_config_1.CliArgs).parse(new rules_config_1.CliUsage('wp-finish-push-dev', 'Commit a resolved dev composition and publish the copy. Only needed when `pnpm wp-push-dev --resolve` stopped on a conflict.', [new rules_config_1.CliFlag(ABORT, 'Throw the whole resolve away: undo the halted merge, go back to your feature\n'
17
+ + ' branch, and delete the throwaway branch and state file. Nothing is published.')]));
18
+ await container.get(pr_gate_app_1.PrGateApp).finishPushDev(new finish_push_dev_command_1.FinishPushDevOptions(args.has(ABORT)));
19
+ });
20
+ //# sourceMappingURL=wp-finish-push-dev.js.map