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.
- package/config/service/crewly-watchdog.sh +181 -0
- package/config/service/crewly-watchdog.test.sh +176 -0
- package/config/skills/agent/remote-browser/SKILL.md +1 -1
- package/dist/backend/backend/src/constants.d.ts +28 -1
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +30 -3
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts +9 -0
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js +23 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-ask.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-ask.js +4 -1
- package/dist/backend/backend/src/services/team-move/team-move-ask.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-bundle.d.ts +105 -0
- package/dist/backend/backend/src/services/team-move/team-move-bundle.d.ts.map +1 -0
- package/dist/backend/backend/src/services/team-move/team-move-bundle.js +173 -0
- package/dist/backend/backend/src/services/team-move/team-move-bundle.js.map +1 -0
- package/dist/backend/backend/src/services/team-move/team-move-carry.d.ts +21 -2
- package/dist/backend/backend/src/services/team-move/team-move-carry.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-carry.js +21 -3
- package/dist/backend/backend/src/services/team-move/team-move-carry.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-cloud.client.d.ts +7 -1
- package/dist/backend/backend/src/services/team-move/team-move-cloud.client.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-cloud.client.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-driver.service.d.ts +8 -1
- package/dist/backend/backend/src/services/team-move/team-move-driver.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-driver.service.js +38 -12
- package/dist/backend/backend/src/services/team-move/team-move-driver.service.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-exporter.d.ts +73 -2
- package/dist/backend/backend/src/services/team-move/team-move-exporter.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-exporter.js +222 -27
- package/dist/backend/backend/src/services/team-move/team-move-exporter.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-fs.d.ts +8 -0
- package/dist/backend/backend/src/services/team-move/team-move-fs.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-fs.js +14 -0
- package/dist/backend/backend/src/services/team-move/team-move-fs.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-git.d.ts +95 -2
- package/dist/backend/backend/src/services/team-move/team-move-git.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-git.js +186 -6
- package/dist/backend/backend/src/services/team-move/team-move-git.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-importer.d.ts +41 -3
- package/dist/backend/backend/src/services/team-move/team-move-importer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-importer.js +162 -8
- package/dist/backend/backend/src/services/team-move/team-move-importer.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-preflight.d.ts +13 -1
- package/dist/backend/backend/src/services/team-move/team-move-preflight.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-preflight.js +20 -1
- package/dist/backend/backend/src/services/team-move/team-move-preflight.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-runtime.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-runtime.js +3 -1
- package/dist/backend/backend/src/services/team-move/team-move-runtime.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-secret-scan.d.ts +2 -1
- package/dist/backend/backend/src/services/team-move/team-move-secret-scan.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move-secret-scan.js +4 -1
- package/dist/backend/backend/src/services/team-move/team-move-secret-scan.js.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move.types.d.ts +70 -0
- package/dist/backend/backend/src/services/team-move/team-move.types.d.ts.map +1 -1
- package/dist/backend/backend/src/services/team-move/team-move.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +28 -1
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +30 -3
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/cli/src/commands/service.d.ts +22 -0
- package/dist/cli/cli/src/commands/service.d.ts.map +1 -1
- package/dist/cli/cli/src/commands/service.js +211 -1
- package/dist/cli/cli/src/commands/service.js.map +1 -1
- package/dist/cli/cli/src/commands/stop.d.ts.map +1 -1
- package/dist/cli/cli/src/commands/stop.js +4 -0
- package/dist/cli/cli/src/commands/stop.js.map +1 -1
- package/dist/cli/cli/src/utils/shutdown-marker.d.ts +9 -0
- package/dist/cli/cli/src/utils/shutdown-marker.d.ts.map +1 -1
- package/dist/cli/cli/src/utils/shutdown-marker.js +19 -0
- package/dist/cli/cli/src/utils/shutdown-marker.js.map +1 -1
- 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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH,OAAO,KAAK,EAAE,OAAO,EAAoB,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAI5E,OAAO,EAA0B,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAIxF,OAAO,EAAE,WAAW,EAAa,MAAM,mBAAmB,CAAC;AAC3D,OAAO,
|
|
1
|
+
{"version":3,"file":"team-move-exporter.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/team-move/team-move-exporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH,OAAO,KAAK,EAAE,OAAO,EAAoB,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAI5E,OAAO,EAA0B,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAIxF,OAAO,EAAE,WAAW,EAAa,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAkD,KAAK,WAAW,EAAgD,MAAM,sBAAsB,CAAC;AACtJ,OAAO,EAAqE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC9I,OAAO,EAAwC,WAAW,IAAI,kBAAkB,EAAmC,MAAM,oBAAoB,CAAC;AAC9I,OAAO,EAA4D,KAAK,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAU1H,OAAO,KAAK,EAEV,oBAAoB,EACpB,gBAAgB,EAGhB,WAAW,EAIX,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,sBAAsB,CAAC;AAiC9B,0EAA0E;AAC1E,qBAAa,oBAAqB,SAAQ,KAAK;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE;gBAAf,KAAK,EAAE,MAAM,EAAE;CAIrC;AAED,qCAAqC;AACrC,MAAM,WAAW,UAAU;IACzB,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAChC,WAAW,EAAE,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACjE,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAC/C,WAAW,CAAC,EAAE,OAAO,kBAAkB,CAAC;IACxC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,oFAAoF;IACpF,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0EAA0E;IAC1E,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,kFAAkF;IAClF,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,4EAA4E;IAC5E,aAAa,CAAC,EAAE,iBAAiB,CAAC;CACnC;AAED,mDAAmD;AACnD,MAAM,WAAW,iBAAiB;IAChC,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,iBAAiB,CAMhG;AAgBD,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,cAAc,CAM1F;AAED,gHAAgH;AAChH,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAExD;AAED,6BAA6B;AAC7B,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gHAAgH;IAChH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yGAAyG;IACzG,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gGAAgG;IAChG,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,4HAA4H;IAC5H,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,wDAAwD;AACxD,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,+BAA+B;AAC/B,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,OAAO,EAAE,WAAW,CAAC;IACrB,+CAA+C;IAC/C,OAAO,EAAE,oBAAoB,CAAC;IAC9B,2EAA2E;IAC3E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,gFAAgF;IAChF,YAAY,EAAE,eAAe,EAAE,CAAC;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzF;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,UAAU,CAAC,EAAE,MAAM,EAAE;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAO1L;AAOD,oBAAoB;AACpB,qBAAa,gBAAgB;IAKf,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJjC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;gBAEvB,IAAI,EAAE,UAAU;IAM7C,0EAA0E;IAC1E,OAAO,CAAC,UAAU;IAIlB,iEAAiE;IACjE,OAAO,CAAC,SAAS;YAIH,UAAU;IAUxB;;;;;;;;;;;;;OAaG;YACW,SAAS;IAkDvB,oHAAoH;YACtG,WAAW;IAQzB,oFAAoF;IACpF,OAAO,CAAC,QAAQ;IAQhB;;;;;;;;;;OAUG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAUvF,yGAAyG;IACzG,OAAO,CAAC,KAAK,CAAoM;IAEjN;;;;;;;;;;OAUG;IACG,SAAS,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,qBAAqB,CAAC;YA2FvN,WAAW;YAOX,YAAY;IAK1B;;;;;;OAMG;IACG,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IAuK3D;;;;;;OAMG;YACW,YAAY;IAyB1B;;;;;OAKG;YACW,UAAU;IAiBxB,mGAAmG;YACrF,iBAAiB;IAkC/B,+FAA+F;IAC/F,OAAO,CAAC,aAAa;IAmBrB,qEAAqE;IACrE,OAAO,CAAC,YAAY;IAWpB,wCAAwC;YAC1B,cAAc;IAkC5B;;;;OAIG;YACW,eAAe;IAwE7B;;;;;OAKG;YACW,gBAAgB;IAqC9B,4FAA4F;YAC9E,WAAW;CAe1B;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,gBAAgB,GAAG,oBAAoB,CAe1E;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAWtJ;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,EAAE,CAEzE"}
|
|
@@ -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 {
|
|
39
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 >
|
|
779
|
+
if (c.size > projectLimits.fileBytes)
|
|
585
780
|
skip('too_large');
|
|
586
781
|
else if (c.size > remaining)
|
|
587
782
|
skip('size_cap');
|