@codions/portta 0.0.0-develop.157.sha-ae5ce5e → 0.0.0-develop.162.sha-7b40859

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.
package/dist/host.js CHANGED
@@ -46312,6 +46312,9 @@ var AvailableBranchSchema = external_exports.object({
46312
46312
  var AvailableBranchesQuerySchema = external_exports.object({
46313
46313
  includeRemote: BooleanLikeSchema.optional()
46314
46314
  });
46315
+ var RemoveWorktreeQuerySchema = external_exports.object({
46316
+ force: BooleanLikeSchema.optional()
46317
+ });
46315
46318
  var NumberLikePathParamSchema = external_exports.union([
46316
46319
  external_exports.number().int().nonnegative(),
46317
46320
  external_exports.string().regex(/^\d+$/).transform((value) => Number(value))
@@ -47245,6 +47248,7 @@ var apiContract = defineRoutes({
47245
47248
  summary: "Remove a worktree",
47246
47249
  path: apiPaths.removeWorktree,
47247
47250
  pathParams: WorktreeNameParamsSchema,
47251
+ query: RemoveWorktreeQuerySchema,
47248
47252
  responses: {
47249
47253
  200: OkResponseSchema,
47250
47254
  ...commonErrorResponses
@@ -49084,6 +49088,21 @@ var NodeGitGateway = class {
49084
49088
  const result = tryRunGit(["diff", "HEAD", "--no-color"], cwd);
49085
49089
  return result.ok ? result.stdout : "";
49086
49090
  }
49091
+ /**
49092
+ * Commits reachable from HEAD and from nothing else that would survive this
49093
+ * worktree: not on an origin branch, not on the project's main branch. It
49094
+ * answers "would deleting this branch lose work", which `aheadCount` cannot:
49095
+ * with no upstream and no remote at all, every commit counts as ahead.
49096
+ */
49097
+ countUnsavedCommits(cwd, baseBranch) {
49098
+ const count = (args) => {
49099
+ const result = tryRunGit(args, cwd);
49100
+ return result.ok ? parseInt(result.stdout, 10) || 0 : null;
49101
+ };
49102
+ const excludingBase = baseBranch ? count(["rev-list", "--count", "HEAD", "--not", "--remotes=origin", baseBranch]) : null;
49103
+ if (excludingBase !== null) return excludingBase;
49104
+ return count(["rev-list", "--count", "HEAD", "--not", "--remotes=origin"]) ?? 0;
49105
+ }
49087
49106
  listUnpushedCommits(cwd) {
49088
49107
  let result = tryRunGit(["log", "--oneline", "@{upstream}..HEAD"], cwd);
49089
49108
  if (!result.ok) {
@@ -52664,9 +52683,23 @@ var LifecycleService = class {
52664
52683
  throw this.wrapOperationError(error61);
52665
52684
  }
52666
52685
  }
52667
- async removeWorktree(branch) {
52686
+ /**
52687
+ * Removing a worktree deletes its branch with `--force`, so anything living
52688
+ * only there is gone. Uncommitted changes and unpushed commits therefore
52689
+ * refuse the removal unless the caller says to discard them.
52690
+ */
52691
+ async removeWorktree(branch, options = {}) {
52668
52692
  try {
52669
52693
  const resolved2 = await this.resolveExistingWorktree(branch);
52694
+ if (!options.force) {
52695
+ const unsaved = this.describeUnsavedWork(resolved2.entry);
52696
+ if (unsaved) {
52697
+ throw new LifecycleError(
52698
+ `Worktree has ${unsaved}: ${branch}. Commit or push them, or remove with --force to discard them.`,
52699
+ 409
52700
+ );
52701
+ }
52702
+ }
52670
52703
  await this.removeResolvedWorktree(resolved2);
52671
52704
  } catch (error61) {
52672
52705
  throw this.wrapOperationError(error61);
@@ -52677,15 +52710,21 @@ var LifecycleService = class {
52677
52710
  const resolvedWorktrees = await this.resolveAllWorktrees();
52678
52711
  const sessionName = buildProjectSessionName(this.deps.projectRoot);
52679
52712
  const removedBranches = [];
52713
+ const keptBranches = [];
52680
52714
  for (const resolved2 of resolvedWorktrees) {
52681
52715
  const branch = resolved2.entry.branch ?? resolved2.entry.path;
52682
52716
  if (await this.deps.sessions.hasWindow(sessionName, buildWorktreeWindowName(branch))) {
52683
52717
  continue;
52684
52718
  }
52719
+ const unsaved = this.describeUnsavedWork(resolved2.entry);
52720
+ if (unsaved) {
52721
+ keptBranches.push({ branch, reason: unsaved });
52722
+ continue;
52723
+ }
52685
52724
  await this.removeResolvedWorktree(resolved2);
52686
52725
  removedBranches.push(branch);
52687
52726
  }
52688
- return { removedBranches };
52727
+ return { removedBranches, keptBranches };
52689
52728
  } catch (error61) {
52690
52729
  throw this.wrapOperationError(error61);
52691
52730
  }
@@ -53182,6 +53221,14 @@ ${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
53182
53221
  }
53183
53222
  return cleanupErrors.length > 0 ? cleanupErrors.join("; ") : null;
53184
53223
  }
53224
+ /** What this worktree holds that exists nowhere else, or null when it is safe to delete. */
53225
+ describeUnsavedWork(entry) {
53226
+ const status = this.deps.git.readWorktreeStatus(entry.path);
53227
+ if (status.dirty) return "uncommitted changes";
53228
+ const commits = this.deps.git.countUnsavedCommits(entry.path, this.deps.config.workspace.mainBranch);
53229
+ if (commits > 0) return `${commits} commit${commits === 1 ? "" : "s"} that are only here`;
53230
+ return null;
53231
+ }
53185
53232
  ensureNoUncommittedChanges(entry) {
53186
53233
  const status = this.deps.git.readWorktreeStatus(entry.path);
53187
53234
  if (status.dirty) {
@@ -53278,7 +53325,7 @@ ${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
53278
53325
  const cleanupErrors = [];
53279
53326
  for (const branch of [...branches].reverse()) {
53280
53327
  try {
53281
- await this.removeWorktree(branch);
53328
+ await this.removeWorktree(branch, { force: true });
53282
53329
  } catch (error61) {
53283
53330
  cleanupErrors.push(`rollback failed for ${branch}: ${toErrorMessage2(error61)}`);
53284
53331
  }
@@ -58969,14 +59016,14 @@ ${resolvedPrompt}` : conversationContext;
58969
59016
  201
58970
59017
  );
58971
59018
  }
58972
- async function apiDeleteWorktree(project, name) {
59019
+ async function apiDeleteWorktree(project, name, force) {
58973
59020
  return project.withRemovingBranch(name, async () => {
58974
59021
  log.info(`[worktree:rm] name=${name}`);
58975
59022
  const worktree = project.runtime.projectRuntime.getWorktreeByBranch(name);
58976
59023
  const environmentId = worktree ? environmentIdForWorkspace(project.environments.projectEnvironments(), worktree.worktreeId) : null;
58977
59024
  const environment = environmentId ? project.environments.environmentForProject(environmentId) : null;
58978
59025
  if (environment) await project.environments.destroyEnvironmentRecord(environment);
58979
- await project.runtime.lifecycleService.removeWorktree(name);
59026
+ await project.runtime.lifecycleService.removeWorktree(name, { force });
58980
59027
  log.debug(`[worktree:rm] done name=${name}`);
58981
59028
  return jsonResponse({ ok: true });
58982
59029
  });
@@ -59163,9 +59210,9 @@ function worktreeRoutes(deps) {
59163
59210
  const app = projectRouter();
59164
59211
  projectRoute(app, deps, "fetchWorktrees", "Worktrees", (project) => apiGetWorktrees(project));
59165
59212
  projectRoute(app, deps, "createWorktree", "Worktrees", (project, { body }) => apiCreateWorktree(project, body));
59166
- projectRoute(app, deps, "removeWorktree", "Worktrees", (project, { params }) => {
59213
+ projectRoute(app, deps, "removeWorktree", "Worktrees", (project, { params, query: query2 }) => {
59167
59214
  const name = worktreeName(params.name);
59168
- return name instanceof Response ? name : apiDeleteWorktree(project, name);
59215
+ return name instanceof Response ? name : apiDeleteWorktree(project, name, query2.force === true);
59169
59216
  });
59170
59217
  projectRoute(app, deps, "openWorktree", "Worktrees", (project, { params, body }) => {
59171
59218
  const name = worktreeName(params.name);
@@ -72223,7 +72270,7 @@ function createDaemonTaskflowHost(context) {
72223
72270
  port: hostListen(env2).port,
72224
72271
  workflowBuiltinsDir: taskflowAssets(env2).workflowBuiltinsDir,
72225
72272
  version: env2.PORTTA_VERSION || "0.0.0-local",
72226
- buildDate: true ? "2026-09-16T21:32:25Z" : (/* @__PURE__ */ new Date()).toISOString(),
72273
+ buildDate: true ? "2026-09-17T23:23:40Z" : (/* @__PURE__ */ new Date()).toISOString(),
72227
72274
  environmentStore: createEnvironmentStore(paths.database),
72228
72275
  hostEnvironmentProvider: new HostEnvironmentProvider(),
72229
72276
  devContainerProvider: new DevContainerProvider(),
@@ -1 +1 @@
1
- 0.0.0-develop.157.sha-ae5ce5e
1
+ 0.0.0-develop.162.sha-7b40859
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codions/portta",
3
- "version": "0.0.0-develop.157.sha-ae5ce5e",
3
+ "version": "0.0.0-develop.162.sha-7b40859",
4
4
  "description": "Machine-first CLI for a shared Docker development gateway",
5
5
  "license": "MIT",
6
6
  "type": "module",