crewly 1.20.313 → 1.20.315

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 (76) hide show
  1. package/config/service/crewly-watchdog.sh +181 -0
  2. package/config/service/crewly-watchdog.test.sh +176 -0
  3. package/config/skills/agent/remote-browser/SKILL.md +1 -1
  4. package/dist/backend/backend/src/constants.d.ts +28 -1
  5. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  6. package/dist/backend/backend/src/constants.js +30 -3
  7. package/dist/backend/backend/src/constants.js.map +1 -1
  8. package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts +9 -0
  9. package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -1
  10. package/dist/backend/backend/src/services/browser/browser-proxy.service.js +23 -1
  11. package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -1
  12. package/dist/backend/backend/src/services/team-move/team-move-ask.d.ts.map +1 -1
  13. package/dist/backend/backend/src/services/team-move/team-move-ask.js +4 -1
  14. package/dist/backend/backend/src/services/team-move/team-move-ask.js.map +1 -1
  15. package/dist/backend/backend/src/services/team-move/team-move-bundle.d.ts +105 -0
  16. package/dist/backend/backend/src/services/team-move/team-move-bundle.d.ts.map +1 -0
  17. package/dist/backend/backend/src/services/team-move/team-move-bundle.js +173 -0
  18. package/dist/backend/backend/src/services/team-move/team-move-bundle.js.map +1 -0
  19. package/dist/backend/backend/src/services/team-move/team-move-carry.d.ts +21 -2
  20. package/dist/backend/backend/src/services/team-move/team-move-carry.d.ts.map +1 -1
  21. package/dist/backend/backend/src/services/team-move/team-move-carry.js +21 -3
  22. package/dist/backend/backend/src/services/team-move/team-move-carry.js.map +1 -1
  23. package/dist/backend/backend/src/services/team-move/team-move-cloud.client.d.ts +7 -1
  24. package/dist/backend/backend/src/services/team-move/team-move-cloud.client.d.ts.map +1 -1
  25. package/dist/backend/backend/src/services/team-move/team-move-cloud.client.js.map +1 -1
  26. package/dist/backend/backend/src/services/team-move/team-move-driver.service.d.ts +8 -1
  27. package/dist/backend/backend/src/services/team-move/team-move-driver.service.d.ts.map +1 -1
  28. package/dist/backend/backend/src/services/team-move/team-move-driver.service.js +38 -12
  29. package/dist/backend/backend/src/services/team-move/team-move-driver.service.js.map +1 -1
  30. package/dist/backend/backend/src/services/team-move/team-move-exporter.d.ts +73 -2
  31. package/dist/backend/backend/src/services/team-move/team-move-exporter.d.ts.map +1 -1
  32. package/dist/backend/backend/src/services/team-move/team-move-exporter.js +222 -27
  33. package/dist/backend/backend/src/services/team-move/team-move-exporter.js.map +1 -1
  34. package/dist/backend/backend/src/services/team-move/team-move-fs.d.ts +8 -0
  35. package/dist/backend/backend/src/services/team-move/team-move-fs.d.ts.map +1 -1
  36. package/dist/backend/backend/src/services/team-move/team-move-fs.js +14 -0
  37. package/dist/backend/backend/src/services/team-move/team-move-fs.js.map +1 -1
  38. package/dist/backend/backend/src/services/team-move/team-move-git.d.ts +95 -2
  39. package/dist/backend/backend/src/services/team-move/team-move-git.d.ts.map +1 -1
  40. package/dist/backend/backend/src/services/team-move/team-move-git.js +186 -6
  41. package/dist/backend/backend/src/services/team-move/team-move-git.js.map +1 -1
  42. package/dist/backend/backend/src/services/team-move/team-move-importer.d.ts +41 -3
  43. package/dist/backend/backend/src/services/team-move/team-move-importer.d.ts.map +1 -1
  44. package/dist/backend/backend/src/services/team-move/team-move-importer.js +162 -8
  45. package/dist/backend/backend/src/services/team-move/team-move-importer.js.map +1 -1
  46. package/dist/backend/backend/src/services/team-move/team-move-preflight.d.ts +13 -1
  47. package/dist/backend/backend/src/services/team-move/team-move-preflight.d.ts.map +1 -1
  48. package/dist/backend/backend/src/services/team-move/team-move-preflight.js +20 -1
  49. package/dist/backend/backend/src/services/team-move/team-move-preflight.js.map +1 -1
  50. package/dist/backend/backend/src/services/team-move/team-move-runtime.d.ts.map +1 -1
  51. package/dist/backend/backend/src/services/team-move/team-move-runtime.js +3 -1
  52. package/dist/backend/backend/src/services/team-move/team-move-runtime.js.map +1 -1
  53. package/dist/backend/backend/src/services/team-move/team-move-secret-scan.d.ts +2 -1
  54. package/dist/backend/backend/src/services/team-move/team-move-secret-scan.d.ts.map +1 -1
  55. package/dist/backend/backend/src/services/team-move/team-move-secret-scan.js +4 -1
  56. package/dist/backend/backend/src/services/team-move/team-move-secret-scan.js.map +1 -1
  57. package/dist/backend/backend/src/services/team-move/team-move.types.d.ts +70 -0
  58. package/dist/backend/backend/src/services/team-move/team-move.types.d.ts.map +1 -1
  59. package/dist/backend/backend/src/services/team-move/team-move.types.js.map +1 -1
  60. package/dist/backend/build-info.json +2 -2
  61. package/dist/cli/backend/src/constants.d.ts +28 -1
  62. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  63. package/dist/cli/backend/src/constants.js +30 -3
  64. package/dist/cli/backend/src/constants.js.map +1 -1
  65. package/dist/cli/cli/src/commands/service.d.ts +22 -0
  66. package/dist/cli/cli/src/commands/service.d.ts.map +1 -1
  67. package/dist/cli/cli/src/commands/service.js +211 -1
  68. package/dist/cli/cli/src/commands/service.js.map +1 -1
  69. package/dist/cli/cli/src/commands/stop.d.ts.map +1 -1
  70. package/dist/cli/cli/src/commands/stop.js +4 -0
  71. package/dist/cli/cli/src/commands/stop.js.map +1 -1
  72. package/dist/cli/cli/src/utils/shutdown-marker.d.ts +9 -0
  73. package/dist/cli/cli/src/utils/shutdown-marker.d.ts.map +1 -1
  74. package/dist/cli/cli/src/utils/shutdown-marker.js +19 -0
  75. package/dist/cli/cli/src/utils/shutdown-marker.js.map +1 -1
  76. package/package.json +1 -1
@@ -27,6 +27,7 @@ import type { Project, Team } from '../../types/index.js';
27
27
  import { type SecretEnvValue } from '../../utils/secret-env.js';
28
28
  import { StagingArea } from './team-move-fs.js';
29
29
  import { type CarryLimits } from './team-move-carry.js';
30
+ import { type BundleLimits, type BundleMode } from './team-move-bundle.js';
30
31
  import { inspectRepo as defaultInspectRepo } from './team-move-git.js';
31
32
  import { type SecretFinding } from './team-move-secret-scan.js';
32
33
  import type { CloudManifestSummary, PackageFileEntry, PackageRepo, SourcePreflightReport, TeamMoveManifest } from './team-move.types.js';
@@ -52,9 +53,28 @@ export interface ExportDeps {
52
53
  now?: () => string;
53
54
  /** Caps for the working files of non-git projects; default from constants / env. */
54
55
  workLimits?: WorkFileLimits;
55
- /** Caps for carried uncommitted changes; default from constants. */
56
+ /** Caps for carried uncommitted changes; default from constants / env. */
56
57
  carryLimits?: CarryLimits;
58
+ /** Caps for repositories carried as git bundles; default from constants / env. */
59
+ bundleLimits?: BundleLimits;
60
+ /** Caps for project-local `.crewly` files; default from constants / env. */
61
+ projectLimits?: ProjectFileLimits;
57
62
  }
63
+ /** Size caps for project-local `.crewly` files. */
64
+ export interface ProjectFileLimits {
65
+ /** All projects together. */
66
+ totalBytes: number;
67
+ /** One file. */
68
+ fileBytes: number;
69
+ }
70
+ /**
71
+ * The caps for project `.crewly` files: constants, overridable in megabytes by
72
+ * `CREWLY_TEAM_MOVE_PROJECT_FILES_MAX_MB` and `CREWLY_TEAM_MOVE_PROJECT_FILE_MAX_MB`.
73
+ *
74
+ * @param env - Environment to read overrides from
75
+ * @returns The caps in bytes
76
+ */
77
+ export declare function resolveProjectFileLimits(env?: NodeJS.ProcessEnv): ProjectFileLimits;
58
78
  /** Size caps for the working files of projects that are not git repositories. */
59
79
  export interface WorkFileLimits {
60
80
  /** All such projects together. */
@@ -86,6 +106,8 @@ export interface ExportOptions {
86
106
  carryChanges?: boolean;
87
107
  /** Build and scan the package but never throw for secrets; report every finding (preflight). */
88
108
  dryRun?: boolean;
109
+ /** Remotes the target machine cannot read (its `git ls-remote` failed): those repositories travel whole as a git bundle. */
110
+ unreachableRemotes?: readonly string[];
89
111
  }
90
112
  /** A secret-scan finding with its owner-facing name. */
91
113
  export interface LabelledFinding extends SecretFinding {
@@ -111,6 +133,18 @@ export interface ExportResult {
111
133
  * @returns Copy with runtime state cleared
112
134
  */
113
135
  export declare function sanitizeTeamForMove(raw: Record<string, unknown>): Record<string, unknown>;
136
+ /**
137
+ * What the owner reads about a repository that travels in the package.
138
+ *
139
+ * @param repo - The repository record
140
+ * @param bundle - Its bundle plan
141
+ * @returns One line, e.g. `no remote — will be carried in the package (0.8 GB)`
142
+ */
143
+ export declare function bundleMessage(repo: Pick<PackageRepo, 'remoteUrl' | 'unpushed'>, bundle: {
144
+ mode: BundleMode;
145
+ estimate: number | null;
146
+ unreachable?: boolean;
147
+ }, targetName?: string): string;
114
148
  /** The exporter. */
115
149
  export declare class TeamMoveExporter {
116
150
  private readonly deps;
@@ -118,10 +152,37 @@ export declare class TeamMoveExporter {
118
152
  private readonly now;
119
153
  private readonly inspect;
120
154
  constructor(deps: ExportDeps);
155
+ /** Whether a repo record is a git repository with at least one commit. */
156
+ private hasCommits;
157
+ /** Whether a repository holds work the remote (if any) lacks. */
158
+ private needsWork;
159
+ private planBundle;
160
+ /**
161
+ * Decide how each repository travels.
162
+ *
163
+ * - No remote: the repository is carried as a full git bundle plus its
164
+ * working-tree changes (always; there is nothing to clone).
165
+ * - Remote, nothing local: cloned on the target.
166
+ * - Remote, local work and `assess`: uncommitted work and local commits are
167
+ * flattened into patches when that fits; otherwise the unpushed commits
168
+ * travel as a git bundle and the working-tree changes as patches on top.
169
+ *
170
+ * @param repos - Inspected repositories
171
+ * @param assess - Also assess carrying for repositories that have a remote
172
+ * @returns Plan by project id
173
+ */
174
+ private planRepos;
175
+ /** Record the size of each repository that will be cloned on the target, so it can scale the clone's time limit. */
176
+ private recordSizes;
177
+ /** Put a plan's outcome on the repo record (what `check` and the manifest show). */
178
+ private annotate;
121
179
  /**
122
180
  * Check, before anything is built, that no repository holds work only this
123
181
  * machine has. Used by the REST entry point so the owner learns what to
124
- * push before a move is even requested.
182
+ * push before a move is even requested. A repository without a remote does
183
+ * not block: it is carried in the package as a git bundle (its record says
184
+ * "no remote — will be carried in the package (x GB)"), unless it is over
185
+ * the size caps.
125
186
  *
126
187
  * @param teamId - Team to move
127
188
  * @returns The repos and the lines that block the move
@@ -150,6 +211,8 @@ export declare class TeamMoveExporter {
150
211
  force?: boolean;
151
212
  ttlMs?: number;
152
213
  quick?: boolean;
214
+ unreachableRemotes?: readonly string[];
215
+ targetName?: string;
153
216
  }): Promise<SourcePreflightReport>;
154
217
  private requireTeam;
155
218
  private teamProjects;
@@ -161,6 +224,14 @@ export declare class TeamMoveExporter {
161
224
  * @throws TeamMoveBlockedError for unpushed work; TeamMoveSecretScanError for secrets
162
225
  */
163
226
  export(options: ExportOptions): Promise<ExportResult>;
227
+ /**
228
+ * Create the git bundle of each repository that travels as one, straight
229
+ * into the staging directory (streamed by git; never held in memory). A dry
230
+ * run (preflight) builds none.
231
+ *
232
+ * @throws TeamMoveBlockedError when a bundle cannot be made (over the caps, ...) and the move is not forced
233
+ */
234
+ private writeBundles;
164
235
  /**
165
236
  * Write the patches and untracked files of carried repositories into the
166
237
  * package.
@@ -1 +1 @@
1
- {"version":3,"file":"team-move-exporter.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/team-move/team-move-exporter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"team-move-exporter.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/team-move/team-move-exporter.ts"],"names":[],"mappings":"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"}
@@ -35,8 +35,9 @@ import { PACKAGE_TREE_STORES, getCrewlyHomePath, isInsidePackageTree, resolvePro
35
35
  import { readTeamChat } from './team-move-chat.js';
36
36
  import { createEncryptStream, createHashTap } from './team-move-crypto.js';
37
37
  import { StagingArea, walkFiles } from './team-move-fs.js';
38
- import { DEFAULT_CARRY_LIMITS, collectCarry, describeLeft } from './team-move-carry.js';
39
- import { describeBlockingRepos, displayRemote, inspectRepo as defaultInspectRepo, readRepoChanges } from './team-move-git.js';
38
+ import { collectCarry, describeLeft, resolveCarryLimits } from './team-move-carry.js';
39
+ import { createBundle, estimateRepoBytes, formatBytes, resolveBundleLimits } from './team-move-bundle.js';
40
+ import { describeBlockingRepos, displayRemote, inspectRepo as defaultInspectRepo, readRepoChanges, sanitizeRemote } from './team-move-git.js';
40
41
  import { TeamMoveSecretScanError, isSkippableFinding, scanStaging } from './team-move-secret-scan.js';
41
42
  import { identityOf, resetWorkItemForTarget, selectDecisions, selectPoolItems, selectScheduledMessages, } from './team-move-select.js';
42
43
  const C = TEAM_MOVE_CONSTANTS;
@@ -75,6 +76,20 @@ export class TeamMoveBlockedError extends Error {
75
76
  this.name = 'TeamMoveBlockedError';
76
77
  }
77
78
  }
79
+ /**
80
+ * The caps for project `.crewly` files: constants, overridable in megabytes by
81
+ * `CREWLY_TEAM_MOVE_PROJECT_FILES_MAX_MB` and `CREWLY_TEAM_MOVE_PROJECT_FILE_MAX_MB`.
82
+ *
83
+ * @param env - Environment to read overrides from
84
+ * @returns The caps in bytes
85
+ */
86
+ export function resolveProjectFileLimits(env = process.env) {
87
+ const mb = (name, fallback) => {
88
+ const n = Number(env[name]);
89
+ return Number.isFinite(n) && n > 0 ? Math.floor(n * 1024 * 1024) : fallback;
90
+ };
91
+ return { totalBytes: mb(C.PROJECT_FILES_MAX_MB_ENV, C.PROJECT_FILES_MAX_BYTES), fileBytes: mb(C.PROJECT_FILE_MAX_MB_ENV, C.PROJECT_FILE_MAX_BYTES) };
92
+ }
78
93
  /**
79
94
  * The caps for non-git project files: constants, overridable in megabytes by
80
95
  * `CREWLY_TEAM_MOVE_WORK_MAX_MB` and `CREWLY_TEAM_MOVE_WORK_FILE_MAX_MB`.
@@ -113,6 +128,22 @@ export function sanitizeTeamForMove(raw) {
113
128
  });
114
129
  return team;
115
130
  }
131
+ /**
132
+ * What the owner reads about a repository that travels in the package.
133
+ *
134
+ * @param repo - The repository record
135
+ * @param bundle - Its bundle plan
136
+ * @returns One line, e.g. `no remote — will be carried in the package (0.8 GB)`
137
+ */
138
+ export function bundleMessage(repo, bundle, targetName) {
139
+ const size = bundle.estimate !== null ? ` (${formatBytes(bundle.estimate)})` : '';
140
+ if (bundle.mode === 'full' && bundle.unreachable) {
141
+ return `${targetName ?? 'the target machine'} can't reach this private repo; it will be carried in the package${size}. Log in to GitHub on ${targetName ?? 'that machine'} later to push/pull.`;
142
+ }
143
+ if (bundle.mode === 'full')
144
+ return `no remote — will be carried in the package${size}`;
145
+ return `${repo.unpushed} commit${repo.unpushed === 1 ? '' : 's'} not pushed — will be carried in the package as a git bundle${size}`;
146
+ }
116
147
  /** Escape a string for use in a RegExp. */
117
148
  function escapeRegExp(text) {
118
149
  return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -129,10 +160,117 @@ export class TeamMoveExporter {
129
160
  this.now = deps.now ?? (() => new Date().toISOString());
130
161
  this.inspect = deps.inspectRepo ?? defaultInspectRepo;
131
162
  }
163
+ /** Whether a repo record is a git repository with at least one commit. */
164
+ hasCommits(r) {
165
+ return !!r.headSha && !r.note?.startsWith('not a git repository') && !r.note?.startsWith('project folder does not exist');
166
+ }
167
+ /** Whether a repository holds work the remote (if any) lacks. */
168
+ needsWork(r) {
169
+ return r.dirty || r.unpushed > 0;
170
+ }
171
+ async planBundle(mode, repoPath, limits) {
172
+ // A full bundle is about the size of the object store; the size of an
173
+ // unpushed-commits bundle cannot be known cheaply and is checked once built.
174
+ const estimate = mode === 'full' ? await estimateRepoBytes(repoPath) : null;
175
+ if (estimate !== null && estimate > limits.repoBytes) {
176
+ return { mode, estimate, possible: false, reason: `the repository is about ${formatBytes(estimate)}, over the ${formatBytes(limits.repoBytes)} limit for carrying it (raise ${C.BUNDLE_REPO_MAX_MB_ENV} to allow it)` };
177
+ }
178
+ return { mode, estimate, possible: true };
179
+ }
180
+ /**
181
+ * Decide how each repository travels.
182
+ *
183
+ * - No remote: the repository is carried as a full git bundle plus its
184
+ * working-tree changes (always; there is nothing to clone).
185
+ * - Remote, nothing local: cloned on the target.
186
+ * - Remote, local work and `assess`: uncommitted work and local commits are
187
+ * flattened into patches when that fits; otherwise the unpushed commits
188
+ * travel as a git bundle and the working-tree changes as patches on top.
189
+ *
190
+ * @param repos - Inspected repositories
191
+ * @param assess - Also assess carrying for repositories that have a remote
192
+ * @returns Plan by project id
193
+ */
194
+ async planRepos(repos, assess, unreachableRemotes = []) {
195
+ const unreachable = new Set(unreachableRemotes.map(sanitizeRemote));
196
+ const carryLimits = this.deps.carryLimits ?? resolveCarryLimits();
197
+ const bundleLimits = this.deps.bundleLimits ?? resolveBundleLimits();
198
+ const plans = new Map();
199
+ let bundleTotal = 0;
200
+ for (const r of repos) {
201
+ if (!this.hasCommits(r)) {
202
+ plans.set(r.projectId, { resolved: false });
203
+ continue;
204
+ }
205
+ const cannotClone = !!r.remoteUrl && unreachable.has(sanitizeRemote(r.remoteUrl));
206
+ if (!r.remoteUrl || cannotClone || (assess && this.needsWork(r))) {
207
+ let plan = null;
208
+ if (r.remoteUrl && !cannotClone) {
209
+ const flat = await collectCarry(r.path, carryLimits);
210
+ if (flat.ok && flat.carry.left.length === 0)
211
+ plan = { carry: flat.carry, carryRes: flat, resolved: true };
212
+ else if (r.unpushed === 0)
213
+ plan = { carryRes: flat, carryError: flat.ok ? describeLeft(flat.carry.left) : flat.reason, resolved: false };
214
+ }
215
+ if (!plan) {
216
+ const mode = r.remoteUrl && !cannotClone ? 'unpushed' : 'full';
217
+ const bundle = await this.planBundle(mode, r.path, bundleLimits);
218
+ if (cannotClone)
219
+ bundle.unreachable = true;
220
+ if (bundle.possible && bundle.estimate !== null && bundleTotal + bundle.estimate > bundleLimits.totalBytes) {
221
+ bundle.possible = false;
222
+ bundle.reason = `the repositories to carry total about ${formatBytes(bundleTotal + bundle.estimate)}, over the ${formatBytes(bundleLimits.totalBytes)} limit (raise ${C.BUNDLES_MAX_MB_ENV} to allow it)`;
223
+ }
224
+ if (!bundle.possible) {
225
+ plan = { bundle, carryError: bundle.reason, resolved: false };
226
+ }
227
+ else {
228
+ bundleTotal += bundle.estimate ?? 0;
229
+ const res = await collectCarry(r.path, carryLimits, { atHead: true });
230
+ const ok = res.ok && res.carry.left.length === 0;
231
+ plan = {
232
+ bundle,
233
+ carryRes: res,
234
+ ...(res.ok && ok ? { carry: res.carry } : {}),
235
+ ...(ok ? {} : { carryError: res.ok ? describeLeft(res.carry.left) : res.reason }),
236
+ resolved: ok,
237
+ };
238
+ }
239
+ }
240
+ plans.set(r.projectId, plan);
241
+ }
242
+ else {
243
+ plans.set(r.projectId, { resolved: false });
244
+ }
245
+ }
246
+ return plans;
247
+ }
248
+ /** Record the size of each repository that will be cloned on the target, so it can scale the clone's time limit. */
249
+ async recordSizes(repos) {
250
+ for (const r of repos) {
251
+ if (!r.remoteUrl || r.bundle || !r.headSha)
252
+ continue;
253
+ const bytes = await estimateRepoBytes(r.path);
254
+ if (bytes !== null && bytes > 0)
255
+ r.sizeBytes = bytes;
256
+ }
257
+ }
258
+ /** Put a plan's outcome on the repo record (what `check` and the manifest show). */
259
+ annotate(repo, plan) {
260
+ if (plan.bundle && plan.resolved) {
261
+ repo.bundle = { mode: plan.bundle.mode, bytes: plan.bundle.estimate ?? 0, ...(plan.bundle.unreachable ? { why: 'remote_unreachable' } : {}) };
262
+ repo.note = bundleMessage(repo, plan.bundle);
263
+ }
264
+ if (plan.carryError)
265
+ repo.carryError = plan.carryError;
266
+ }
132
267
  /**
133
268
  * Check, before anything is built, that no repository holds work only this
134
269
  * machine has. Used by the REST entry point so the owner learns what to
135
- * push before a move is even requested.
270
+ * push before a move is even requested. A repository without a remote does
271
+ * not block: it is carried in the package as a git bundle (its record says
272
+ * "no remote — will be carried in the package (x GB)"), unless it is over
273
+ * the size caps.
136
274
  *
137
275
  * @param teamId - Team to move
138
276
  * @returns The repos and the lines that block the move
@@ -141,7 +279,11 @@ export class TeamMoveExporter {
141
279
  const team = await this.requireTeam(teamId);
142
280
  const projects = await this.teamProjects(team);
143
281
  const repos = await Promise.all(projects.map((p) => this.inspect({ id: p.id, name: p.name, path: p.path })));
144
- return { repos, blocking: describeBlockingRepos(repos) };
282
+ // A repository without a remote travels in the package, so it does not block.
283
+ const plans = await this.planRepos(repos, false);
284
+ for (const r of repos)
285
+ this.annotate(r, plans.get(r.projectId));
286
+ return { repos, blocking: describeBlockingRepos(repos.filter((r) => !plans.get(r.projectId).resolved)) };
145
287
  }
146
288
  /** The heavy part of the last preflight (package dry run, secret scan), reused while nothing changed. */
147
289
  heavy = null;
@@ -160,22 +302,32 @@ export class TeamMoveExporter {
160
302
  const team = await this.requireTeam(input.teamId);
161
303
  const projects = await this.teamProjects(team);
162
304
  const inspected = await Promise.all(projects.map((p) => this.inspect({ id: p.id, name: p.name, path: p.path })));
163
- const limits = this.deps.carryLimits ?? DEFAULT_CARRY_LIMITS;
305
+ const plans = await this.planRepos(inspected, true, input.unreachableRemotes);
164
306
  const repos = [];
165
307
  for (const r of inspected) {
166
308
  const isRepo = !r.note?.startsWith('not a git repository') && !r.note?.startsWith('project folder does not exist');
167
309
  const changes = isRepo && (r.dirty || r.unpushed > 0) ? await readRepoChanges(r.path) : { modified: 0, untracked: 0, files: [] };
310
+ const plan = plans.get(r.projectId);
168
311
  let carry = null;
169
312
  if (isRepo && (r.dirty || r.unpushed > 0)) {
170
- if (!r.remoteUrl)
171
- carry = { possible: false, files: 0, bytes: 0, flattenedCommits: 0, reason: 'this repository has no remote' };
313
+ const res = plan.carryRes;
314
+ if (!res)
315
+ carry = { possible: false, files: 0, bytes: 0, flattenedCommits: 0, reason: plan.carryError ?? 'this repository cannot be carried' };
172
316
  else {
173
- const res = await collectCarry(r.path, limits);
174
317
  carry = res.ok
175
318
  ? { possible: res.carry.left.length === 0, files: res.carry.patches.length + res.carry.untracked.length, bytes: res.carry.bytes, flattenedCommits: res.carry.flattenedCommits, ...(res.carry.left.length ? { reason: describeLeft(res.carry.left) } : {}) }
176
319
  : { possible: false, files: 0, bytes: 0, flattenedCommits: 0, reason: res.reason };
177
320
  }
178
321
  }
322
+ const bundle = plan.bundle
323
+ ? {
324
+ mode: plan.bundle.mode,
325
+ bytes: plan.bundle.estimate,
326
+ possible: plan.bundle.possible,
327
+ ...(plan.bundle.reason ? { reason: plan.bundle.reason } : {}),
328
+ message: plan.bundle.possible ? bundleMessage(r, plan.bundle, input.targetName) : `${plan.bundle.unreachable ? 'the target cannot reach the remote' : r.remoteUrl ? 'commits not pushed' : 'no remote'} — cannot be carried: ${plan.bundle.reason}`,
329
+ }
330
+ : null;
179
331
  const top = changes.files.slice(0, C.PREFLIGHT_TOP_FILES);
180
332
  repos.push({
181
333
  projectId: r.projectId,
@@ -184,6 +336,8 @@ export class TeamMoveExporter {
184
336
  isRepo,
185
337
  branch: r.branch,
186
338
  remote: displayRemote(r.remoteUrl),
339
+ ...(r.remoteUrl ? { remoteUrl: sanitizeRemote(r.remoteUrl) } : {}),
340
+ ...(plan.bundle?.unreachable ? { remoteUnreachable: true } : {}),
187
341
  noRemote: isRepo && !r.remoteUrl,
188
342
  modified: changes.modified,
189
343
  untracked: changes.untracked,
@@ -193,15 +347,16 @@ export class TeamMoveExporter {
193
347
  topFiles: top,
194
348
  filesTruncated: changes.files.length > top.length,
195
349
  carry,
350
+ bundle,
196
351
  });
197
352
  }
198
- const key = JSON.stringify([team.id, !!input.carryChanges, inspected.map((r, i) => [r.path, r.headSha, r.dirty, r.unpushed, repos[i].modified, repos[i].untracked, repos[i].topFiles.map((f) => f.path)])]);
353
+ const key = JSON.stringify([team.id, !!input.carryChanges, [...(input.unreachableRemotes ?? [])].sort(), inspected.map((r, i) => [r.path, r.headSha, r.dirty, r.unpushed, repos[i].modified, repos[i].untracked, repos[i].topFiles.map((f) => f.path)])]);
199
354
  const ttl = input.ttlMs ?? C.PREFLIGHT_SCAN_TTL_MS;
200
355
  let heavy = !input.force && this.heavy && this.heavy.key === key && Date.now() - this.heavy.at < ttl ? this.heavy : null;
201
356
  if (!heavy && !input.quick) {
202
357
  const staging = path.resolve(input.stagingDir);
203
358
  try {
204
- const result = await this.export({ teamId: team.id, moveId: `preflight-${Date.now()}`, stagingDir: staging, allowDirty: true, carryChanges: !!input.carryChanges, dryRun: true });
359
+ const result = await this.export({ teamId: team.id, moveId: `preflight-${Date.now()}`, stagingDir: staging, allowDirty: true, carryChanges: !!input.carryChanges, dryRun: true, unreachableRemotes: input.unreachableRemotes });
205
360
  const findings = result.scanFindings.slice(0, C.PREFLIGHT_MAX_FINDINGS).map((f) => ({ file: f.label, matches: f.matches, removable: f.removable }));
206
361
  heavy = {
207
362
  key,
@@ -257,20 +412,19 @@ export class TeamMoveExporter {
257
412
  const projects = await this.teamProjects(team);
258
413
  const id = identityOf(team);
259
414
  const repos = await Promise.all(projects.map((p) => this.inspect({ id: p.id, name: p.name, path: p.path })));
260
- // Uncommitted work that can travel in the package no longer blocks the move.
415
+ // Work that can travel in the package no longer blocks the move: uncommitted
416
+ // changes, and whole repositories that have no remote (git bundle).
417
+ const plans = await this.planRepos(repos, !!options.carryChanges, options.unreachableRemotes);
261
418
  const carried = new Map();
262
- if (options.carryChanges) {
263
- for (const repo of repos) {
264
- if (!repo.remoteUrl || !(repo.dirty || repo.unpushed > 0))
265
- continue;
266
- const res = await collectCarry(repo.path, this.deps.carryLimits ?? DEFAULT_CARRY_LIMITS);
267
- if (res.ok && res.carry.left.length === 0)
268
- carried.set(repo.projectId, res.carry);
269
- else
270
- repo.carryError = res.ok ? describeLeft(res.carry.left) : res.reason;
271
- }
419
+ for (const repo of repos) {
420
+ const plan = plans.get(repo.projectId);
421
+ this.annotate(repo, plan);
422
+ // A bundled repository with a clean working tree has nothing to patch.
423
+ if (plan.carry && (!plan.bundle || plan.carry.patches.length + plan.carry.untracked.length > 0))
424
+ carried.set(repo.projectId, plan.carry);
272
425
  }
273
- const blocking = describeBlockingRepos(repos.filter((r) => !carried.has(r.projectId)));
426
+ await this.recordSizes(repos);
427
+ const blocking = describeBlockingRepos(repos.filter((r) => !plans.get(r.projectId).resolved));
274
428
  if (blocking.length > 0 && !options.allowDirty)
275
429
  throw new TeamMoveBlockedError(blocking);
276
430
  const staging = new StagingArea(path.resolve(options.stagingDir));
@@ -345,7 +499,8 @@ export class TeamMoveExporter {
345
499
  const requiredRuntimes = [...new Set(team.members.map((m) => m.runtimeType).filter(Boolean))].sort();
346
500
  // 7. Secret scan: nothing leaves the machine with a credential in it.
347
501
  const known = this.deps.knownSecrets ? await this.deps.knownSecrets() : collectSecretEnvValues(process.env);
348
- const scan = await scanStaging(staging, staging.files(), known);
502
+ // Bundles are not text-scanned: they are binary packs (history that already lives in git), encrypted for the target.
503
+ const scan = await scanStaging(staging, staging.files(), known, (p) => p.endsWith(`/${C.BUNDLE_FILE}`));
349
504
  const scanFindings = this.labelFindings(scan.findings, projects, carried);
350
505
  let excludedFiles = 0;
351
506
  if (scan.findings.length > 0 && !options.dryRun) {
@@ -374,6 +529,8 @@ export class TeamMoveExporter {
374
529
  }
375
530
  }
376
531
  }
532
+ // 7a. Repositories carried as git bundles (written by git straight into the package).
533
+ await this.writeBundles(staging, repos, plans, options);
377
534
  // 7b. Carried changes: the index lists what is really in the package.
378
535
  await this.writeCarryIndexes(staging, repos, carried, carriedLost);
379
536
  // 8. Manifest.
@@ -402,7 +559,8 @@ export class TeamMoveExporter {
402
559
  requiredRuntimes,
403
560
  requiredConnectors,
404
561
  files,
405
- totalBytes: files.reduce((n, f) => n + f.bytes, 0),
562
+ // A dry run builds no bundle; its estimate keeps the preflight size honest.
563
+ totalBytes: files.reduce((n, f) => n + f.bytes, 0) + (options.dryRun ? repos.reduce((n, r) => n + (r.bundle && !r.bundle.file ? r.bundle.bytes : 0), 0) : 0),
406
564
  noSecrets: true,
407
565
  secretScan: { filesScanned: scan.filesScanned, knownValuesChecked: known.length, findings: 0, excludedFiles },
408
566
  notMoved: this.notMovedList(),
@@ -416,6 +574,42 @@ export class TeamMoveExporter {
416
574
  scanFindings,
417
575
  };
418
576
  }
577
+ /**
578
+ * Create the git bundle of each repository that travels as one, straight
579
+ * into the staging directory (streamed by git; never held in memory). A dry
580
+ * run (preflight) builds none.
581
+ *
582
+ * @throws TeamMoveBlockedError when a bundle cannot be made (over the caps, ...) and the move is not forced
583
+ */
584
+ async writeBundles(staging, repos, plans, options) {
585
+ if (options.dryRun)
586
+ return;
587
+ const limits = this.deps.bundleLimits ?? resolveBundleLimits();
588
+ let remaining = limits.totalBytes;
589
+ const failures = [];
590
+ for (const repo of repos) {
591
+ const plan = plans.get(repo.projectId);
592
+ if (!plan.bundle || !plan.resolved)
593
+ continue;
594
+ const rel = `projects/${repo.projectId}/${C.BUNDLE_FILE}`;
595
+ const res = await createBundle(repo.path, staging.abs(rel), plan.bundle.mode, Math.min(limits.repoBytes, remaining));
596
+ if (!res.ok) {
597
+ delete repo.bundle;
598
+ repo.carryError = res.reason;
599
+ failures.push(`${repo.projectName} (${repo.path}): ${res.reason}`);
600
+ continue;
601
+ }
602
+ const entry = await staging.adopt(rel);
603
+ remaining -= entry.bytes;
604
+ repo.bundle = { mode: plan.bundle.mode, file: rel, bytes: entry.bytes, sha256: entry.sha256, refs: res.refs };
605
+ if (plan.bundle.mode === 'full')
606
+ repo.note = bundleMessage(repo, { ...plan.bundle, estimate: entry.bytes });
607
+ if (plan.bundle.unreachable)
608
+ repo.bundle.why = 'remote_unreachable';
609
+ }
610
+ if (failures.length > 0 && !options.allowDirty)
611
+ throw new TeamMoveBlockedError(failures);
612
+ }
419
613
  /**
420
614
  * Write the patches and untracked files of carried repositories into the
421
615
  * package.
@@ -504,7 +698,7 @@ export class TeamMoveExporter {
504
698
  return [
505
699
  'Credentials, API keys, OAuth/connector grants and runtime logins (Claude, Codex, ...)',
506
700
  'Slack bot tokens (Crewly Cloud re-points the agents\' Slack apps to the target)',
507
- 'Source code of git projects (cloned from each repository on the target; projects that are not git repositories travel in the package)',
701
+ 'Source code of git projects that have a remote (cloned from it on the target; projects that are not git repositories, and repositories without a remote or with unpushed commits, travel in the package)',
508
702
  'Chat message history (channels and rooms move, messages stay on the source)',
509
703
  'Running sessions, logs, transcripts, build output and node_modules',
510
704
  'Short-lived agent reminders (they are set again by the agents)',
@@ -545,7 +739,8 @@ export class TeamMoveExporter {
545
739
  */
546
740
  async collectProjects(staging, projects, allTeams, teamId, counts) {
547
741
  const out = [];
548
- let remaining = C.PROJECT_FILES_MAX_BYTES;
742
+ const projectLimits = this.deps.projectLimits ?? resolveProjectFileLimits();
743
+ let remaining = projectLimits.totalBytes;
549
744
  for (const project of projects) {
550
745
  const dataDir = resolveProjectDataDir(project.path);
551
746
  const inHome = isInsidePackageTree(project.path);
@@ -581,7 +776,7 @@ export class TeamMoveExporter {
581
776
  const skip = (reason) => {
582
777
  entry.skipped.push({ path: c.rel, bytes: c.size, reason });
583
778
  };
584
- if (c.size > C.PROJECT_FILE_MAX_BYTES)
779
+ if (c.size > projectLimits.fileBytes)
585
780
  skip('too_large');
586
781
  else if (c.size > remaining)
587
782
  skip('size_cap');