@savvy-web/silk-effects 5.8.1 → 5.9.1
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/changesets/services/release-planner.js +4 -7
- package/changesets/utils/version-files.js +117 -78
- package/index.d.ts +88 -6
- package/package.json +3 -3
- package/repos/index.js +6 -4
- package/repos/schemas/reports.js +18 -1
- package/repos/services/drift.js +8 -2
- package/repos/services/manager.js +73 -5
|
@@ -338,13 +338,10 @@ function applyEffect(root, dryRun, changelogModules, inspector, fs) {
|
|
|
338
338
|
version: fresh
|
|
339
339
|
} : p;
|
|
340
340
|
});
|
|
341
|
-
versionFileUpdates = yield* Effect.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
reason: errMsg(e)
|
|
346
|
-
})
|
|
347
|
-
});
|
|
341
|
+
versionFileUpdates = scopes.length > 0 ? yield* VersionFiles.processResolvedVersionFiles(scopes, dryRun).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.mapError((e) => new ReleasePlanError({
|
|
342
|
+
phase: "apply",
|
|
343
|
+
reason: errMsg(e)
|
|
344
|
+
}))) : [];
|
|
348
345
|
}
|
|
349
346
|
return {
|
|
350
347
|
dryRun,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { VersionFileError } from "../errors.js";
|
|
1
2
|
import { LegacyVersionFilesSchema } from "../schemas/version-files.js";
|
|
2
3
|
import { jsonPathGet, jsonPathResolve, parseJsonPath } from "./jsonpath.js";
|
|
3
|
-
import { Effect, Path, Schema } from "effect";
|
|
4
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
5
5
|
import { join, relative, resolve } from "node:path";
|
|
6
6
|
import { GlobPatternOptions } from "@effected/glob";
|
|
7
7
|
import { compileAndExpand } from "@effected/walker";
|
|
@@ -102,34 +102,32 @@ var VersionFiles = class VersionFiles {
|
|
|
102
102
|
* @returns Array of workspace packages with versions
|
|
103
103
|
*/
|
|
104
104
|
static discoverVersions(cwd, packages) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
name: pkg.name,
|
|
113
|
-
path: pkg.path,
|
|
114
|
-
version: pkg.version
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
if (!seen.has(resolvedCwd)) {
|
|
118
|
-
const version = readPackageVersion(resolvedCwd);
|
|
119
|
-
if (version) {
|
|
120
|
-
let rootName = "root";
|
|
121
|
-
try {
|
|
122
|
-
const pkg = JSON.parse(readFileSync(join(resolvedCwd, "package.json"), "utf-8"));
|
|
123
|
-
if (pkg.name) rootName = pkg.name;
|
|
124
|
-
} catch {}
|
|
105
|
+
return Effect.gen(function* () {
|
|
106
|
+
const resolvedCwd = resolve(cwd);
|
|
107
|
+
const results = [];
|
|
108
|
+
const seen = /* @__PURE__ */ new Set();
|
|
109
|
+
for (const pkg of packages) {
|
|
110
|
+
if (seen.has(pkg.path) || !pkg.version) continue;
|
|
111
|
+
seen.add(pkg.path);
|
|
125
112
|
results.push({
|
|
126
|
-
name:
|
|
127
|
-
path:
|
|
128
|
-
version
|
|
113
|
+
name: pkg.name,
|
|
114
|
+
path: pkg.path,
|
|
115
|
+
version: pkg.version
|
|
129
116
|
});
|
|
130
117
|
}
|
|
131
|
-
|
|
132
|
-
|
|
118
|
+
if (!seen.has(resolvedCwd)) {
|
|
119
|
+
const version = yield* readPackageVersion(resolvedCwd);
|
|
120
|
+
if (version) {
|
|
121
|
+
const rootName = yield* readPackageName(resolvedCwd);
|
|
122
|
+
results.push({
|
|
123
|
+
name: rootName,
|
|
124
|
+
path: resolvedCwd,
|
|
125
|
+
version
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return results;
|
|
130
|
+
});
|
|
133
131
|
}
|
|
134
132
|
/**
|
|
135
133
|
* Determine which workspace version applies to a given file path
|
|
@@ -224,16 +222,19 @@ var VersionFiles = class VersionFiles {
|
|
|
224
222
|
* @see {@link VersionFiles.applyVersionEdit} for the per-path edit
|
|
225
223
|
*/
|
|
226
224
|
static updateFile(filePath, jsonPaths, version) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
filePath,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
225
|
+
return Effect.gen(function* () {
|
|
226
|
+
const fs = yield* FileSystem.FileSystem;
|
|
227
|
+
const original = yield* fs.readFileString(filePath);
|
|
228
|
+
const { content, previousValues, totalChanged } = VersionFiles.computeUpdate(original, jsonPaths, version);
|
|
229
|
+
if (totalChanged === 0) return;
|
|
230
|
+
yield* fs.writeFileString(filePath, content);
|
|
231
|
+
return {
|
|
232
|
+
filePath,
|
|
233
|
+
jsonPaths,
|
|
234
|
+
version,
|
|
235
|
+
previousValues
|
|
236
|
+
};
|
|
237
|
+
});
|
|
237
238
|
}
|
|
238
239
|
/**
|
|
239
240
|
* Compute the full update for a document without touching the filesystem:
|
|
@@ -330,30 +331,15 @@ var VersionFiles = class VersionFiles {
|
|
|
330
331
|
*/
|
|
331
332
|
static processVersionFiles(cwd, configs, dryRun = false, packages = []) {
|
|
332
333
|
return Effect.gen(function* () {
|
|
333
|
-
const workspaces = VersionFiles.discoverVersions(cwd, packages);
|
|
334
|
+
const workspaces = yield* VersionFiles.discoverVersions(cwd, packages);
|
|
334
335
|
const rootVersion = workspaces.find((ws) => ws.path === resolve(cwd))?.version ?? "0.0.0";
|
|
335
336
|
const resolved = yield* VersionFiles.resolveGlobs(configs, cwd);
|
|
336
337
|
const updates = [];
|
|
337
338
|
for (const [filePath, config] of resolved) {
|
|
338
339
|
const jsonPaths = config.paths ?? ["$.version"];
|
|
339
340
|
const version = config.package ? workspaces.find((ws) => ws.name === config.package)?.version ?? rootVersion : VersionFiles.resolveVersion(filePath, workspaces, rootVersion);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const content = readFileSync(filePath, "utf-8");
|
|
343
|
-
const { previousValues, totalChanged } = VersionFiles.computeUpdate(content, jsonPaths, version);
|
|
344
|
-
if (totalChanged > 0) updates.push({
|
|
345
|
-
filePath,
|
|
346
|
-
jsonPaths,
|
|
347
|
-
version,
|
|
348
|
-
previousValues
|
|
349
|
-
});
|
|
350
|
-
} else {
|
|
351
|
-
const result = VersionFiles.updateFile(filePath, jsonPaths, version);
|
|
352
|
-
if (result) updates.push(result);
|
|
353
|
-
}
|
|
354
|
-
} catch (error) {
|
|
355
|
-
throw new Error(`Failed to update ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
356
|
-
}
|
|
341
|
+
const result = yield* VersionFiles.applyOne(filePath, jsonPaths, version, dryRun).pipe(Effect.orDie);
|
|
342
|
+
if (result) updates.push(result);
|
|
357
343
|
}
|
|
358
344
|
return updates;
|
|
359
345
|
});
|
|
@@ -380,28 +366,61 @@ var VersionFiles = class VersionFiles {
|
|
|
380
366
|
* @internal
|
|
381
367
|
*/
|
|
382
368
|
static processResolvedVersionFiles(scopes, dryRun = false) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
const { previousValues, totalChanged } = VersionFiles.computeUpdate(content, jsonPaths, scope.version);
|
|
390
|
-
if (totalChanged > 0) updates.push({
|
|
391
|
-
filePath,
|
|
392
|
-
jsonPaths,
|
|
393
|
-
version: scope.version,
|
|
394
|
-
previousValues
|
|
395
|
-
});
|
|
396
|
-
} else {
|
|
397
|
-
const result = VersionFiles.updateFile(filePath, jsonPaths, scope.version);
|
|
369
|
+
return Effect.gen(function* () {
|
|
370
|
+
const updates = [];
|
|
371
|
+
for (const scope of scopes) for (const vf of scope.versionFiles) {
|
|
372
|
+
const jsonPaths = vf.paths.length > 0 ? vf.paths : ["$.version"];
|
|
373
|
+
for (const filePath of vf.matchedFiles) {
|
|
374
|
+
const result = yield* VersionFiles.applyOne(filePath, jsonPaths, scope.version, dryRun);
|
|
398
375
|
if (result) updates.push(result);
|
|
399
376
|
}
|
|
400
|
-
} catch (error) {
|
|
401
|
-
throw new Error(`Failed to update ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
402
377
|
}
|
|
378
|
+
return updates;
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Apply one version file's update, shared by both process entry points.
|
|
383
|
+
*
|
|
384
|
+
* @remarks
|
|
385
|
+
* Fails TYPED with `VersionFileError`, carrying the offending `filePath`. The two callers then
|
|
386
|
+
* choose their own posture, which is not the same and must not be unified:
|
|
387
|
+
* {@link VersionFiles.processVersionFiles} (legacy) turns it into a DEFECT,
|
|
388
|
+
* matching the synchronous throw it had under `node:fs`, while
|
|
389
|
+
* {@link VersionFiles.processResolvedVersionFiles} leaves it typed because
|
|
390
|
+
* `ReleasePlanner.apply` converts it to a `ReleasePlanError` — a defect there
|
|
391
|
+
* would bypass the inspector's catch and crash `apply()`.
|
|
392
|
+
*
|
|
393
|
+
* @internal
|
|
394
|
+
*/
|
|
395
|
+
static applyOne(filePath, jsonPaths, version, dryRun) {
|
|
396
|
+
return Effect.gen(function* () {
|
|
397
|
+
if (dryRun) {
|
|
398
|
+
const content = yield* (yield* FileSystem.FileSystem).readFileString(filePath);
|
|
399
|
+
const { previousValues, totalChanged } = VersionFiles.computeUpdate(content, jsonPaths, version);
|
|
400
|
+
return totalChanged > 0 ? {
|
|
401
|
+
filePath,
|
|
402
|
+
jsonPaths,
|
|
403
|
+
version,
|
|
404
|
+
previousValues
|
|
405
|
+
} : void 0;
|
|
406
|
+
}
|
|
407
|
+
return yield* VersionFiles.updateFile(filePath, jsonPaths, version);
|
|
408
|
+
}).pipe(Effect.mapError((error) => new VersionFileError({
|
|
409
|
+
filePath,
|
|
410
|
+
reason: VersionFiles.describeFailure(error)
|
|
411
|
+
})), Effect.catchDefect((defect) => Effect.fail(new VersionFileError({
|
|
412
|
+
filePath,
|
|
413
|
+
reason: VersionFiles.describeFailure(defect)
|
|
414
|
+
}))));
|
|
415
|
+
}
|
|
416
|
+
/** Render a `PlatformError` the way the old `node:fs` catch rendered an `Error`. */
|
|
417
|
+
static describeFailure(error) {
|
|
418
|
+
if (typeof error === "object" && error !== null) {
|
|
419
|
+
const e = error;
|
|
420
|
+
if (typeof e.message === "string") return e.message;
|
|
421
|
+
if (typeof e.reason === "string") return e.reason;
|
|
403
422
|
}
|
|
404
|
-
return
|
|
423
|
+
return String(error);
|
|
405
424
|
}
|
|
406
425
|
};
|
|
407
426
|
/**
|
|
@@ -450,12 +469,32 @@ function expandGlob(source, cwd) {
|
|
|
450
469
|
*
|
|
451
470
|
* @internal
|
|
452
471
|
*/
|
|
472
|
+
/**
|
|
473
|
+
* Read the `name` field from a `package.json` in the given directory, falling
|
|
474
|
+
* back to `"root"` when it is absent, unreadable, or malformed.
|
|
475
|
+
*
|
|
476
|
+
* @param dir - Absolute path to the directory containing `package.json`
|
|
477
|
+
* @returns The package name, or `"root"`
|
|
478
|
+
*
|
|
479
|
+
* @internal
|
|
480
|
+
*/
|
|
481
|
+
function readPackageName(dir) {
|
|
482
|
+
return Effect.gen(function* () {
|
|
483
|
+
const raw = yield* (yield* FileSystem.FileSystem).readFileString(join(dir, "package.json"));
|
|
484
|
+
return yield* Effect.try({
|
|
485
|
+
try: () => JSON.parse(raw).name,
|
|
486
|
+
catch: (e) => e
|
|
487
|
+
});
|
|
488
|
+
}).pipe(Effect.map((name) => name ?? "root"), Effect.orElseSucceed(() => "root"));
|
|
489
|
+
}
|
|
453
490
|
function readPackageVersion(dir) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
491
|
+
return Effect.gen(function* () {
|
|
492
|
+
const raw = yield* (yield* FileSystem.FileSystem).readFileString(join(dir, "package.json"));
|
|
493
|
+
return yield* Effect.try({
|
|
494
|
+
try: () => JSON.parse(raw).version,
|
|
495
|
+
catch: (e) => e
|
|
496
|
+
});
|
|
497
|
+
}).pipe(Effect.orElseSucceed(() => void 0));
|
|
459
498
|
}
|
|
460
499
|
|
|
461
500
|
//#endregion
|
package/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { ChildProcessSpawner } from "effect/unstable/process";
|
|
|
4
4
|
import { PackageManagerDetector, PublishConfig, PublishTarget, PublishabilityDetector, VersioningStrategy, WorkspaceDiscovery, WorkspaceDiscoveryFailure, WorkspacePackage, WorkspaceSnapshotAtFailure, WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshots, WorkspaceStateSnapshot, WorkspacesOptions } from "@effected/workspaces";
|
|
5
5
|
import { Git } from "@effected/git";
|
|
6
6
|
import { GlobExpansionError } from "@effected/walker";
|
|
7
|
+
import * as PlatformError from "effect/PlatformError";
|
|
7
8
|
import { YamlFormattingOptions } from "@effected/yaml";
|
|
8
9
|
import { Section, SectionId } from "@effected/templates";
|
|
9
10
|
import { ToolDiscovery } from "@effected/commands";
|
|
@@ -4808,7 +4809,7 @@ declare class VersionFiles {
|
|
|
4808
4809
|
name: string;
|
|
4809
4810
|
version: string;
|
|
4810
4811
|
path: string;
|
|
4811
|
-
}>): WorkspaceVersion[]
|
|
4812
|
+
}>): Effect.Effect<WorkspaceVersion[], never, FileSystem.FileSystem>;
|
|
4812
4813
|
/**
|
|
4813
4814
|
* Determine which workspace version applies to a given file path
|
|
4814
4815
|
* using longest-prefix matching.
|
|
@@ -4883,7 +4884,7 @@ declare class VersionFiles {
|
|
|
4883
4884
|
* @see {@link jsonPathResolve} for concrete-path enumeration
|
|
4884
4885
|
* @see {@link VersionFiles.applyVersionEdit} for the per-path edit
|
|
4885
4886
|
*/
|
|
4886
|
-
static updateFile(filePath: string, jsonPaths: readonly string[], version: string): VersionFileUpdate | undefined
|
|
4887
|
+
static updateFile(filePath: string, jsonPaths: readonly string[], version: string): Effect.Effect<VersionFileUpdate | undefined, PlatformError.PlatformError, FileSystem.FileSystem>;
|
|
4887
4888
|
/**
|
|
4888
4889
|
* Compute the full update for a document without touching the filesystem:
|
|
4889
4890
|
* the edited content, the previous values at every matched path, and how
|
|
@@ -4965,7 +4966,24 @@ declare class VersionFiles {
|
|
|
4965
4966
|
*
|
|
4966
4967
|
* @internal
|
|
4967
4968
|
*/
|
|
4968
|
-
static processResolvedVersionFiles(scopes: ReadonlyArray<ResolvedPackageScope>, dryRun?: boolean): VersionFileUpdate[]
|
|
4969
|
+
static processResolvedVersionFiles(scopes: ReadonlyArray<ResolvedPackageScope>, dryRun?: boolean): Effect.Effect<VersionFileUpdate[], VersionFileError, FileSystem.FileSystem>;
|
|
4970
|
+
/**
|
|
4971
|
+
* Apply one version file's update, shared by both process entry points.
|
|
4972
|
+
*
|
|
4973
|
+
* @remarks
|
|
4974
|
+
* Fails TYPED with `VersionFileError`, carrying the offending `filePath`. The two callers then
|
|
4975
|
+
* choose their own posture, which is not the same and must not be unified:
|
|
4976
|
+
* {@link VersionFiles.processVersionFiles} (legacy) turns it into a DEFECT,
|
|
4977
|
+
* matching the synchronous throw it had under `node:fs`, while
|
|
4978
|
+
* {@link VersionFiles.processResolvedVersionFiles} leaves it typed because
|
|
4979
|
+
* `ReleasePlanner.apply` converts it to a `ReleasePlanError` — a defect there
|
|
4980
|
+
* would bypass the inspector's catch and crash `apply()`.
|
|
4981
|
+
*
|
|
4982
|
+
* @internal
|
|
4983
|
+
*/
|
|
4984
|
+
private static applyOne;
|
|
4985
|
+
/** Render a `PlatformError` the way the old `node:fs` catch rendered an `Error`. */
|
|
4986
|
+
private static describeFailure;
|
|
4969
4987
|
}
|
|
4970
4988
|
//#endregion
|
|
4971
4989
|
//#region ../../node_modules/.pnpm/micromark-util-types@2.0.2/node_modules/micromark-util-types/index.d.ts
|
|
@@ -9188,6 +9206,25 @@ declare const ReposRestoreResult: Schema.Struct<{
|
|
|
9188
9206
|
}>;
|
|
9189
9207
|
/** @public */
|
|
9190
9208
|
type ReposRestoreResult = typeof ReposRestoreResult.Type;
|
|
9209
|
+
/**
|
|
9210
|
+
* Result of clearing a stale submodule registration from the superproject's
|
|
9211
|
+
* LOCAL git config.
|
|
9212
|
+
*
|
|
9213
|
+
* @remarks
|
|
9214
|
+
* `section` is the registration name the section was cleared under (e.g.
|
|
9215
|
+
* `.repos/effect-old`), and `removedKeys` lists the full config keys the
|
|
9216
|
+
* removed section actually carried — the achieved-state report, read from the
|
|
9217
|
+
* config BEFORE the removal so a caller can see exactly what is gone. There
|
|
9218
|
+
* is no `commitMessage`: the local config is unversioned, so a deregister
|
|
9219
|
+
* stages nothing and leaves nothing to commit.
|
|
9220
|
+
* @public
|
|
9221
|
+
*/
|
|
9222
|
+
declare const ReposDeregisterResult: Schema.Struct<{
|
|
9223
|
+
readonly section: Schema.String;
|
|
9224
|
+
readonly removedKeys: Schema.$Array<Schema.String>;
|
|
9225
|
+
}>;
|
|
9226
|
+
/** @public */
|
|
9227
|
+
type ReposDeregisterResult = typeof ReposDeregisterResult.Type;
|
|
9191
9228
|
/**
|
|
9192
9229
|
* Result of an agent-note mutation against a vendored repo.
|
|
9193
9230
|
* @public
|
|
@@ -9233,6 +9270,24 @@ declare class ReposConfigStore extends ReposConfigStore_base {
|
|
|
9233
9270
|
}
|
|
9234
9271
|
//#endregion
|
|
9235
9272
|
//#region src/repos/services/drift.d.ts
|
|
9273
|
+
/**
|
|
9274
|
+
* Extracts the submodule NAME from a `submodule.<name>.<property>` config key.
|
|
9275
|
+
*
|
|
9276
|
+
* @remarks
|
|
9277
|
+
* A submodule's name is routinely a PATH (`git submodule add` derives the
|
|
9278
|
+
* section name from the path), so the name itself contains dots — which means
|
|
9279
|
+
* the key cannot be split on `.` and read positionally. Strip the fixed
|
|
9280
|
+
* `submodule.` prefix and the final `.<property>` segment instead; everything
|
|
9281
|
+
* between is the name, dots and all. Returns `undefined` for any key that is
|
|
9282
|
+
* not a `submodule.*.*` key.
|
|
9283
|
+
*
|
|
9284
|
+
* Shared with `ReposManager.deregister`, which must attribute config keys to
|
|
9285
|
+
* a registration name the exact same way this check does — two parsers here
|
|
9286
|
+
* would let the report and the remedy disagree about what a section is named.
|
|
9287
|
+
*
|
|
9288
|
+
* @internal
|
|
9289
|
+
*/
|
|
9290
|
+
declare const submoduleNameFromKey: (key: string) => string | undefined;
|
|
9236
9291
|
/**
|
|
9237
9292
|
* The {@link ReposDrift} service shape.
|
|
9238
9293
|
* @public
|
|
@@ -9424,6 +9479,31 @@ interface ReposManagerShape {
|
|
|
9424
9479
|
*/
|
|
9425
9480
|
readonly rename: (root: string, oldName: string, newName: string) => Effect.Effect<ReposRenameResult, ReposConfigError | GitSubmoduleError | RepoNotFoundError | ReposLockdownError>;
|
|
9426
9481
|
readonly restore: (root: string, names?: ReadonlyArray<string>) => Effect.Effect<ReposRestoreResult, ReposConfigError | GitSubmoduleError | RepoNotFoundError | ReposLockdownError>;
|
|
9482
|
+
/**
|
|
9483
|
+
* Clears a STALE submodule registration — a `submodule.<section>.*`
|
|
9484
|
+
* section in the superproject's LOCAL git config left behind by a rename
|
|
9485
|
+
* or an unvendoring, which `git submodule status` then lists as a phantom
|
|
9486
|
+
* entry the manifest has never heard of (`ReposDrift` reports it as
|
|
9487
|
+
* `localRegistrationDivergence`).
|
|
9488
|
+
*
|
|
9489
|
+
* `section` is the registration name exactly as the drift report states it
|
|
9490
|
+
* (e.g. `.repos/effect-old`) — the `submodule.` prefix is implied, never
|
|
9491
|
+
* passed. Refuses (typed `ReposConfigError`): a section outside
|
|
9492
|
+
* `.repos/` (this machinery does not own it — it may be a real submodule
|
|
9493
|
+
* of the host repo), the canonical registration of a live manifest entry,
|
|
9494
|
+
* and a registration whose module gitdir still backs a live entry under a
|
|
9495
|
+
* DIVERGED name (the same gitdir attribution `ReposDrift` uses — that
|
|
9496
|
+
* state's remedy is a re-vendor, not a deregister) — UNLESS that entry's
|
|
9497
|
+
* canonical section is also registered, in which case the old-named twin
|
|
9498
|
+
* is a genuine orphan and removing it is a crashed-rename recovery's own
|
|
9499
|
+
* final step. Reconciling a LIVE
|
|
9500
|
+
* registration is `sync`'s job and unvendoring is `remove`'s, so
|
|
9501
|
+
* deregistration only ever touches state nothing else owns. Touches only
|
|
9502
|
+
* the superproject's local config — no vendored worktree — so unlike the
|
|
9503
|
+
* other mutating ops it needs no lockdown bracket and stages nothing to
|
|
9504
|
+
* commit.
|
|
9505
|
+
*/
|
|
9506
|
+
readonly deregister: (root: string, section: string) => Effect.Effect<ReposDeregisterResult, ReposConfigError | GitSubmoduleError>;
|
|
9427
9507
|
}
|
|
9428
9508
|
declare const ReposManager_base: Context.ServiceClass<ReposManager, "@savvy-web/silk-effects/ReposManager", ReposManagerShape>;
|
|
9429
9509
|
/**
|
|
@@ -9431,8 +9511,10 @@ declare const ReposManager_base: Context.ServiceClass<ReposManager, "@savvy-web/
|
|
|
9431
9511
|
* (presence, dirtiness, stale notes), reconciles the working tree with the
|
|
9432
9512
|
* manifest, vendors new entries (`add`), re-pins existing entries to a new
|
|
9433
9513
|
* ref (`pin`), adds/removes/promotes agent notes (`note`), unvendors
|
|
9434
|
-
* (`remove`), renames (`rename`),
|
|
9435
|
-
* checkouts back to their pinned commit (`restore`)
|
|
9514
|
+
* (`remove`), renames (`rename`), explicitly hard-resets dirty
|
|
9515
|
+
* checkouts back to their pinned commit (`restore`), and clears stale
|
|
9516
|
+
* submodule registrations from the superproject's local git config
|
|
9517
|
+
* (`deregister`).
|
|
9436
9518
|
* @public
|
|
9437
9519
|
*/
|
|
9438
9520
|
declare class ReposManager extends ReposManager_base {
|
|
@@ -9450,7 +9532,7 @@ declare class ReposManager extends ReposManager_base {
|
|
|
9450
9532
|
static readonly layer: Layer.Layer<ReposManager, never, ReposConfigStore | Git | FileSystem.FileSystem | Path.Path | ReposLockdown>;
|
|
9451
9533
|
}
|
|
9452
9534
|
declare namespace index_d_exports$4 {
|
|
9453
|
-
export { DriftKind, GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NOTE_LIMIT, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoDrift, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposConfigStoreShape, ReposDrift, ReposDriftReport, ReposDriftShape, ReposLockdown, ReposLockdownError, ReposLockdownErrorBase, ReposLockdownShape, ReposManager, ReposManagerShape, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS, resolveModuleDir };
|
|
9535
|
+
export { DriftKind, GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NOTE_LIMIT, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoDrift, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposConfigStoreShape, ReposDeregisterResult, ReposDrift, ReposDriftReport, ReposDriftShape, ReposLockdown, ReposLockdownError, ReposLockdownErrorBase, ReposLockdownShape, ReposManager, ReposManagerShape, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS, resolveModuleDir, submoduleNameFromKey };
|
|
9454
9536
|
}
|
|
9455
9537
|
//#endregion
|
|
9456
9538
|
//#region src/schemas/BiomeConfig.d.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvy-web/silk-effects",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared Effect library for Silk Suite conventions",
|
|
6
6
|
"homepage": "https://github.com/savvy-web/systems/tree/main/packages/silk-effects",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"@effected/package-json": "^0.9.0",
|
|
41
41
|
"@effected/templates": "^0.2.0",
|
|
42
42
|
"@effected/walker": "^0.4.0",
|
|
43
|
-
"@effected/workspaces": "^0.13.
|
|
44
|
-
"@effected/yaml": "^0.
|
|
43
|
+
"@effected/workspaces": "^0.13.1",
|
|
44
|
+
"@effected/yaml": "^0.9.0",
|
|
45
45
|
"@manypkg/get-packages": "^3.1.0",
|
|
46
46
|
"mdast-util-heading-range": "^4.0.0",
|
|
47
47
|
"mdast-util-to-string": "^4.0.0",
|
package/repos/index.js
CHANGED
|
@@ -3,10 +3,10 @@ import { MANIFEST_PATH, REPOS_DIR } from "./constants.js";
|
|
|
3
3
|
import { GitSubmoduleError, GitSubmoduleErrorBase, NoteNotFoundError, NoteNotFoundErrorBase, RepoNotFoundError, RepoNotFoundErrorBase, ReposConfigError, ReposConfigErrorBase, ReposLockdownError, ReposLockdownErrorBase } from "./errors.js";
|
|
4
4
|
import { DriftKind, RepoDrift, ReposDriftReport } from "./schemas/drift.js";
|
|
5
5
|
import { RepoEntry, RepoName, RepoNote, RepoOrientation, ReposManifestFile } from "./schemas/manifest.js";
|
|
6
|
-
import { RepoStatusEntry, ReposAddResult, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport } from "./schemas/reports.js";
|
|
6
|
+
import { RepoStatusEntry, ReposAddResult, ReposDeregisterResult, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport } from "./schemas/reports.js";
|
|
7
7
|
import { ReposConfigStore } from "./services/config-store.js";
|
|
8
8
|
import { ReposLockdown, resolveModuleDir } from "./services/lockdown.js";
|
|
9
|
-
import { ReposDrift } from "./services/drift.js";
|
|
9
|
+
import { ReposDrift, submoduleNameFromKey } from "./services/drift.js";
|
|
10
10
|
import { ReposManager, STALE_LOCK_MAX_AGE_MS } from "./services/manager.js";
|
|
11
11
|
|
|
12
12
|
//#region src/repos/index.ts
|
|
@@ -31,6 +31,7 @@ var repos_exports = /* @__PURE__ */ __exportAll({
|
|
|
31
31
|
ReposConfigError: () => ReposConfigError,
|
|
32
32
|
ReposConfigErrorBase: () => ReposConfigErrorBase,
|
|
33
33
|
ReposConfigStore: () => ReposConfigStore,
|
|
34
|
+
ReposDeregisterResult: () => ReposDeregisterResult,
|
|
34
35
|
ReposDrift: () => ReposDrift,
|
|
35
36
|
ReposDriftReport: () => ReposDriftReport,
|
|
36
37
|
ReposLockdown: () => ReposLockdown,
|
|
@@ -46,8 +47,9 @@ var repos_exports = /* @__PURE__ */ __exportAll({
|
|
|
46
47
|
ReposStatusReport: () => ReposStatusReport,
|
|
47
48
|
ReposSyncReport: () => ReposSyncReport,
|
|
48
49
|
STALE_LOCK_MAX_AGE_MS: () => STALE_LOCK_MAX_AGE_MS,
|
|
49
|
-
resolveModuleDir: () => resolveModuleDir
|
|
50
|
+
resolveModuleDir: () => resolveModuleDir,
|
|
51
|
+
submoduleNameFromKey: () => submoduleNameFromKey
|
|
50
52
|
});
|
|
51
53
|
|
|
52
54
|
//#endregion
|
|
53
|
-
export { DriftKind, GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoDrift, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposDrift, ReposDriftReport, ReposLockdown, ReposLockdownError, ReposLockdownErrorBase, ReposManager, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS, repos_exports, resolveModuleDir };
|
|
55
|
+
export { DriftKind, GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoDrift, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposDeregisterResult, ReposDrift, ReposDriftReport, ReposLockdown, ReposLockdownError, ReposLockdownErrorBase, ReposManager, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS, repos_exports, resolveModuleDir, submoduleNameFromKey };
|
package/repos/schemas/reports.js
CHANGED
|
@@ -150,6 +150,23 @@ const ReposRestoreResult = Schema.Struct({
|
|
|
150
150
|
stillDirty: Schema.Array(Schema.String)
|
|
151
151
|
});
|
|
152
152
|
/**
|
|
153
|
+
* Result of clearing a stale submodule registration from the superproject's
|
|
154
|
+
* LOCAL git config.
|
|
155
|
+
*
|
|
156
|
+
* @remarks
|
|
157
|
+
* `section` is the registration name the section was cleared under (e.g.
|
|
158
|
+
* `.repos/effect-old`), and `removedKeys` lists the full config keys the
|
|
159
|
+
* removed section actually carried — the achieved-state report, read from the
|
|
160
|
+
* config BEFORE the removal so a caller can see exactly what is gone. There
|
|
161
|
+
* is no `commitMessage`: the local config is unversioned, so a deregister
|
|
162
|
+
* stages nothing and leaves nothing to commit.
|
|
163
|
+
* @public
|
|
164
|
+
*/
|
|
165
|
+
const ReposDeregisterResult = Schema.Struct({
|
|
166
|
+
section: Schema.String,
|
|
167
|
+
removedKeys: Schema.Array(Schema.String)
|
|
168
|
+
});
|
|
169
|
+
/**
|
|
153
170
|
* Result of an agent-note mutation against a vendored repo.
|
|
154
171
|
* @public
|
|
155
172
|
*/
|
|
@@ -165,4 +182,4 @@ const ReposNoteResult = Schema.Struct({
|
|
|
165
182
|
});
|
|
166
183
|
|
|
167
184
|
//#endregion
|
|
168
|
-
export { RepoStatusEntry, ReposAddResult, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport };
|
|
185
|
+
export { RepoStatusEntry, ReposAddResult, ReposDeregisterResult, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport };
|
package/repos/services/drift.js
CHANGED
|
@@ -17,6 +17,12 @@ import { Git, Gitmodules } from "@effected/git";
|
|
|
17
17
|
* `submodule.` prefix and the final `.<property>` segment instead; everything
|
|
18
18
|
* between is the name, dots and all. Returns `undefined` for any key that is
|
|
19
19
|
* not a `submodule.*.*` key.
|
|
20
|
+
*
|
|
21
|
+
* Shared with `ReposManager.deregister`, which must attribute config keys to
|
|
22
|
+
* a registration name the exact same way this check does — two parsers here
|
|
23
|
+
* would let the report and the remedy disagree about what a section is named.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
20
26
|
*/
|
|
21
27
|
const submoduleNameFromKey = (key) => {
|
|
22
28
|
if (!key.startsWith("submodule.")) return;
|
|
@@ -91,7 +97,7 @@ var ReposDrift = class extends Context.Service()("@savvy-web/silk-effects/ReposD
|
|
|
91
97
|
found.push(RepoDrift.make({
|
|
92
98
|
name: registeredName,
|
|
93
99
|
kind: "localRegistrationDivergence",
|
|
94
|
-
detail: `local git config registers submodule "${registeredName}", which matches no manifest entry — a stale registration left behind by a rename or an unvendoring; clear it with \`
|
|
100
|
+
detail: `local git config registers submodule "${registeredName}", which matches no manifest entry — a stale registration left behind by a rename or an unvendoring; clear it with \`savvy repos deregister ${registeredName}\``,
|
|
95
101
|
observedValue: registeredName
|
|
96
102
|
}));
|
|
97
103
|
}
|
|
@@ -247,4 +253,4 @@ var ReposDrift = class extends Context.Service()("@savvy-web/silk-effects/ReposD
|
|
|
247
253
|
};
|
|
248
254
|
|
|
249
255
|
//#endregion
|
|
250
|
-
export { ReposDrift };
|
|
256
|
+
export { ReposDrift, submoduleNameFromKey };
|
|
@@ -3,6 +3,7 @@ import { GitSubmoduleError, NoteNotFoundError, RepoNotFoundError, ReposConfigErr
|
|
|
3
3
|
import { RepoName } from "../schemas/manifest.js";
|
|
4
4
|
import { ReposConfigStore } from "./config-store.js";
|
|
5
5
|
import { ReposLockdown, resolveModuleDir } from "./lockdown.js";
|
|
6
|
+
import { submoduleNameFromKey } from "./drift.js";
|
|
6
7
|
import { Clock, Context, Effect, Exit, FileSystem, Layer, Option, Path, Result, Schema } from "effect";
|
|
7
8
|
import { Git, GitConfig, Gitmodules, LsRemoteEntry } from "@effected/git";
|
|
8
9
|
import { createHash } from "node:crypto";
|
|
@@ -31,8 +32,10 @@ const STALE_LOCK_MAX_AGE_MS = 6e5;
|
|
|
31
32
|
* (presence, dirtiness, stale notes), reconciles the working tree with the
|
|
32
33
|
* manifest, vendors new entries (`add`), re-pins existing entries to a new
|
|
33
34
|
* ref (`pin`), adds/removes/promotes agent notes (`note`), unvendors
|
|
34
|
-
* (`remove`), renames (`rename`),
|
|
35
|
-
* checkouts back to their pinned commit (`restore`)
|
|
35
|
+
* (`remove`), renames (`rename`), explicitly hard-resets dirty
|
|
36
|
+
* checkouts back to their pinned commit (`restore`), and clears stale
|
|
37
|
+
* submodule registrations from the superproject's local git config
|
|
38
|
+
* (`deregister`).
|
|
36
39
|
* @public
|
|
37
40
|
*/
|
|
38
41
|
var ReposManager = class extends Context.Service()("@savvy-web/silk-effects/ReposManager") {
|
|
@@ -158,12 +161,12 @@ var ReposManager = class extends Context.Service()("@savvy-web/silk-effects/Repo
|
|
|
158
161
|
yield* git.add(root, [".gitmodules", repoPathRel]).pipe(Effect.mapError(asSubmoduleError(`git add .gitmodules ${repoPathRel}`, root)));
|
|
159
162
|
registered.push(name);
|
|
160
163
|
} else if (!present) {
|
|
161
|
-
yield* git.configSet(root, updateKey, "checkout").pipe(Effect.mapError(asSubmoduleError(`git config ${updateKey} checkout`, root)));
|
|
162
164
|
yield* git.submoduleUpdate(root, {
|
|
163
165
|
init: true,
|
|
166
|
+
checkout: true,
|
|
164
167
|
depth: 1,
|
|
165
168
|
paths: [repoPathRel]
|
|
166
|
-
}).pipe(Effect.mapError(asSubmoduleError(`git submodule update --init --depth 1 -- ${repoPathRel}`, root))
|
|
169
|
+
}).pipe(Effect.mapError(asSubmoduleError(`git submodule update --init --checkout --depth 1 -- ${repoPathRel}`, root)));
|
|
167
170
|
initialized.push(name);
|
|
168
171
|
} else upToDate.push(name);
|
|
169
172
|
if (entry.sparse && entry.sparse.length > 0) {
|
|
@@ -647,6 +650,70 @@ var ReposManager = class extends Context.Service()("@savvy-web/silk-effects/Repo
|
|
|
647
650
|
stillDirty
|
|
648
651
|
};
|
|
649
652
|
});
|
|
653
|
+
/**
|
|
654
|
+
* Resolve the file git's `--local` scope actually targets for `root`.
|
|
655
|
+
* A plain checkout is `<root>/.git/config`; a LINKED WORKTREE's
|
|
656
|
+
* `<root>/.git` is a pointer FILE whose `gitdir:` names
|
|
657
|
+
* `.git/worktrees/<name>`, and the local config git uses there is the
|
|
658
|
+
* SHARED one at the main gitdir named by that dir's `commondir` file.
|
|
659
|
+
* Every unreadable/malformed step degrades to the plain
|
|
660
|
+
* `<root>/.git/config` path rather than failing — the same
|
|
661
|
+
* never-fails posture as `resolveModuleDir`.
|
|
662
|
+
*/
|
|
663
|
+
const resolveLocalConfigPath = (root) => Effect.gen(function* () {
|
|
664
|
+
const dotGit = path.join(root, ".git");
|
|
665
|
+
const fallback = path.join(dotGit, "config");
|
|
666
|
+
const info = yield* fs.stat(dotGit).pipe(Effect.option);
|
|
667
|
+
if (Option.isNone(info) || info.value.type === "Directory") return fallback;
|
|
668
|
+
const content = yield* fs.readFileString(dotGit).pipe(Effect.option);
|
|
669
|
+
if (Option.isNone(content)) return fallback;
|
|
670
|
+
const match = /^gitdir:\s*(.+)$/m.exec(content.value);
|
|
671
|
+
if (!match?.[1]) return fallback;
|
|
672
|
+
const pointer = match[1].trim();
|
|
673
|
+
const gitdir = path.isAbsolute(pointer) ? pointer : path.resolve(root, pointer);
|
|
674
|
+
const commondir = yield* fs.readFileString(path.join(gitdir, "commondir")).pipe(Effect.option);
|
|
675
|
+
if (Option.isSome(commondir)) {
|
|
676
|
+
const common = commondir.value.trim();
|
|
677
|
+
return path.join(path.isAbsolute(common) ? common : path.resolve(gitdir, common), "config");
|
|
678
|
+
}
|
|
679
|
+
return path.join(gitdir, "config");
|
|
680
|
+
});
|
|
681
|
+
const deregister = (root, section) => Effect.gen(function* () {
|
|
682
|
+
if (!section.startsWith(`${".repos"}/`) || section === `${".repos"}/`) return yield* Effect.fail(new ReposConfigError({
|
|
683
|
+
path: MANIFEST_PATH,
|
|
684
|
+
reason: `"${section}" is not a ${REPOS_DIR}/ registration — deregister only clears stale vendored-repo sections; clear an unrelated submodule registration with git directly`,
|
|
685
|
+
kind: "invalid"
|
|
686
|
+
}));
|
|
687
|
+
const manifest = yield* configStore.read(root).pipe(Effect.catchTag("ReposConfigError", (error) => error.kind === "missing" ? Effect.succeed({ repos: {} }) : Effect.fail(error)));
|
|
688
|
+
for (const name of Object.keys(manifest.repos)) if (section === `${".repos"}/${name}`) return yield* Effect.fail(new ReposConfigError({
|
|
689
|
+
path: MANIFEST_PATH,
|
|
690
|
+
reason: `"${section}" is the canonical registration of manifest entry "${name}" — deregister clears STALE registrations only; use remove to unvendor, or sync to reconcile`,
|
|
691
|
+
kind: "invalid"
|
|
692
|
+
}));
|
|
693
|
+
const localConfigPath = yield* resolveLocalConfigPath(root);
|
|
694
|
+
const configEntries = yield* git.configList(root, { file: localConfigPath }).pipe(Effect.mapError(asSubmoduleError(`git config -f ${localConfigPath} --list`, root)));
|
|
695
|
+
const registeredNames = new Set(configEntries.map((configEntry) => submoduleNameFromKey(configEntry.key)).filter((registeredName) => registeredName !== void 0));
|
|
696
|
+
const removedKeys = configEntries.map((configEntry) => configEntry.key).filter((key) => submoduleNameFromKey(key) === section);
|
|
697
|
+
if (removedKeys.length === 0) return yield* Effect.fail(new ReposConfigError({
|
|
698
|
+
path: localConfigPath,
|
|
699
|
+
reason: `no submodule.${section} section registered in the local git config — nothing to deregister`,
|
|
700
|
+
kind: "invalid"
|
|
701
|
+
}));
|
|
702
|
+
const modulesRoot = path.join(root, ".git", "modules");
|
|
703
|
+
for (const name of Object.keys(manifest.repos)) {
|
|
704
|
+
const moduleDir = yield* resolveModuleDir(fs, path, root, name);
|
|
705
|
+
if (path.relative(modulesRoot, moduleDir) === section && !registeredNames.has(`${".repos"}/${name}`)) return yield* Effect.fail(new ReposConfigError({
|
|
706
|
+
path: MANIFEST_PATH,
|
|
707
|
+
reason: `"${section}" is the registration backing manifest entry "${name}" (its module gitdir lives there under a diverged name, and no canonical submodule.${REPOS_DIR}/${name} registration exists) — deregister clears STALE registrations only; re-vendor the entry instead (remove, then add with the orientation the remove result hands back)`,
|
|
708
|
+
kind: "invalid"
|
|
709
|
+
}));
|
|
710
|
+
}
|
|
711
|
+
yield* git.configRemoveSection(root, `submodule.${section}`).pipe(Effect.mapError(asSubmoduleError(`git config --remove-section submodule.${section}`, root)));
|
|
712
|
+
return {
|
|
713
|
+
section,
|
|
714
|
+
removedKeys
|
|
715
|
+
};
|
|
716
|
+
});
|
|
650
717
|
return {
|
|
651
718
|
status,
|
|
652
719
|
sync,
|
|
@@ -655,7 +722,8 @@ var ReposManager = class extends Context.Service()("@savvy-web/silk-effects/Repo
|
|
|
655
722
|
note,
|
|
656
723
|
remove,
|
|
657
724
|
rename,
|
|
658
|
-
restore
|
|
725
|
+
restore,
|
|
726
|
+
deregister
|
|
659
727
|
};
|
|
660
728
|
}));
|
|
661
729
|
};
|