@playcraft/cli 0.1.7 → 0.1.8

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.
@@ -596,7 +596,7 @@ async function executeGitCommit(state, scopeKey, operation, payload) {
596
596
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_git_commit' }));
597
597
  }
598
598
  try {
599
- const sha = await commitWorkspaceGit(await workspaceRoot(state, operation.workspaceId), parsed.data);
599
+ const sha = await commitWorkspaceGit(await workspaceRoot(state, operation.workspaceId), parsed.data, boundBranch(state, operation));
600
600
  const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
601
601
  await recordNativeEvents(state, accepted, [
602
602
  {
@@ -617,7 +617,7 @@ async function executeGitPush(state, scopeKey, operation, payload) {
617
617
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'remote_not_configurable' }));
618
618
  }
619
619
  try {
620
- await pushWorkspaceGit(await workspaceRoot(state, operation.workspaceId));
620
+ await pushWorkspaceGit(await workspaceRoot(state, operation.workspaceId), boundBranch(state, operation));
621
621
  const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
622
622
  await recordNativeEvents(state, accepted, [
623
623
  {
@@ -765,6 +765,10 @@ async function executeTerminalCreate(state, scopeKey, operation, payload) {
765
765
  ]);
766
766
  return persist(state, scopeKey, transitionOperation(accepted, 'completed', { terminalId, cwd: record.cwd }));
767
767
  }
768
+ /** The branch a registered workspace is bound to; the single-workspace mode has none. */
769
+ function boundBranch(state, operation) {
770
+ return operation.workspaceId ? state.journal.getWorkspace(operation.workspaceId)?.branchName : undefined;
771
+ }
768
772
  function gitErrorCode(error) {
769
773
  return error instanceof GitWorkspaceError ? error.code : 'git_command_failed';
770
774
  }
@@ -171,8 +171,34 @@ function splitLogLine(line) {
171
171
  return [line.slice(0, first), line.slice(first + 1), ''];
172
172
  return [line.slice(0, first), line.slice(first + 1, second), line.slice(second + 1)];
173
173
  }
174
- export async function commitWorkspaceGit(root, payload) {
174
+ /**
175
+ * Commit and push stay on the branch the workspace is bound to. A switch made in a shell or by an
176
+ * agent is reported, never followed, so work cannot land on another Remix.
177
+ */
178
+ async function assertBoundBranch(rootReal, boundBranch) {
179
+ if (!boundBranch)
180
+ return;
181
+ const current = await gitAllowFail(rootReal, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
182
+ const branch = current.code === 0 ? current.stdout.trim() : '';
183
+ if (branch !== boundBranch) {
184
+ throw new GitWorkspaceError('workspace_branch_mismatch', branch || 'detached HEAD');
185
+ }
186
+ }
187
+ /**
188
+ * A push the remote refused for this ref is a definite outcome, unlike a lost response. Porcelain
189
+ * output marks the ref with "!" and says whether the remote has commits this checkout lacks.
190
+ */
191
+ function pushRejection(stdout) {
192
+ const line = stdout.split('\n').find((item) => item.startsWith('!\t'));
193
+ if (!line)
194
+ return undefined;
195
+ if (/\[rejected\] \((?:non-fast-forward|fetch first)\)/.test(line))
196
+ return 'git_push_remote_ahead';
197
+ return 'git_push_rejected';
198
+ }
199
+ export async function commitWorkspaceGit(root, payload, boundBranch) {
175
200
  const rootReal = await ensureWorkspaceGit(root);
201
+ await assertBoundBranch(rootReal, boundBranch);
176
202
  if (payload.stagedOnly) {
177
203
  // The index is the commit; nothing is added.
178
204
  }
@@ -190,8 +216,9 @@ export async function commitWorkspaceGit(root, payload) {
190
216
  await git(rootReal, ['commit', '-m', payload.message], 20000);
191
217
  return (await git(rootReal, ['rev-parse', 'HEAD'])).trim();
192
218
  }
193
- export async function pushWorkspaceGit(root) {
219
+ export async function pushWorkspaceGit(root, boundBranch) {
194
220
  const rootReal = await ensureWorkspaceGit(root);
221
+ await assertBoundBranch(rootReal, boundBranch);
195
222
  const origin = await gitAllowFail(rootReal, ['remote', 'get-url', 'origin']);
196
223
  if (origin.code !== 0) {
197
224
  throw new GitWorkspaceError('remote_not_configured', 'origin');
@@ -200,12 +227,16 @@ export async function pushWorkspaceGit(root) {
200
227
  if (!ref.startsWith('refs/heads/'))
201
228
  throw new GitWorkspaceError('git_command_failed', 'git_branch_required');
202
229
  const commit = (await git(rootReal, ['rev-parse', '--verify', 'HEAD^{commit}'])).trim();
230
+ let pushed;
203
231
  try {
204
- const pushed = await gitAllowFail(rootReal, ['push', '--porcelain', 'origin', `${commit}:${ref}`], 90000);
205
- if (pushed.code === 0)
206
- return;
232
+ pushed = await gitAllowFail(rootReal, ['push', '--porcelain', 'origin', `${commit}:${ref}`], 90000);
207
233
  }
208
234
  catch { /* A lost response does not prove the remote rejected the update. */ }
235
+ if (pushed?.code === 0)
236
+ return;
237
+ const rejected = pushed ? pushRejection(pushed.stdout) : undefined;
238
+ if (rejected)
239
+ throw new GitWorkspaceError(rejected, rejected);
209
240
  try {
210
241
  const remote = await gitAllowFail(rootReal, ['ls-remote', '--refs', 'origin', ref], 20000);
211
242
  if (remote.code === 0 && remote.stdout.trim() === `${commit}\t${ref}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -48,8 +48,8 @@
48
48
  "@gltf-transform/core": "^4.3.0",
49
49
  "@gltf-transform/extensions": "^4.3.0",
50
50
  "@gltf-transform/functions": "^4.3.0",
51
- "@playcraft/build": "^0.1.7",
52
- "@playcraft/common": "^0.1.7",
51
+ "@playcraft/build": "^0.1.8",
52
+ "@playcraft/common": "^0.1.8",
53
53
  "better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
54
54
  "chokidar": "^4.0.3",
55
55
  "commander": "^13.1.0",