@savvy-web/silk-effects 5.1.1 → 5.2.0
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/README.md +22 -22
- package/changesets/api/transformer.js +2 -1
- package/changesets/changelog/getDependencyReleaseLine.js +24 -5
- package/changesets/changelog/getReleaseLine.js +7 -4
- package/changesets/changelog/index.js +8 -6
- package/changesets/index.js +6 -11
- package/changesets/services/branch-analyzer.js +20 -19
- package/changesets/services/changelog.js +1 -1
- package/changesets/services/config-inspector.js +17 -17
- package/changesets/services/deps-regen.js +36 -34
- package/changesets/services/github.js +39 -40
- package/changesets/services/maintenance-reason.js +5 -1
- package/changesets/services/release-planner.js +7 -6
- package/changesets/utils/dependency-table.js +2 -1
- package/changesets/utils/jsonpath.js +5 -7
- package/changesets/utils/publishability.js +2 -2
- package/commitlint/detection/dco.js +2 -1
- package/commitlint/index.js +2 -1
- package/index.d.ts +254 -218
- package/index.js +7 -7
- package/lint/handlers/PnpmWorkspace.js +2 -1
- package/lint/handlers/Yaml.js +8 -4
- package/package.json +7 -7
- package/repos/index.js +3 -5
- package/repos/services/config-store.js +68 -67
- package/repos/services/manager.js +259 -258
- package/services/BiomeSchemaSync.js +65 -64
- package/services/ChangesetConfig.js +50 -50
- package/services/ChangesetConfigReader.js +59 -58
- package/services/ConfigDiscovery.js +42 -41
- package/services/SilkPublishability.js +54 -53
- package/services/SilkWorkspaceAnalyzer.js +110 -109
- package/turbo/index.js +2 -3
- package/turbo/services/TurboInspector.js +80 -79
|
@@ -24,7 +24,7 @@ const STALE_LOCKS = ["index.lock", "shallow.lock"];
|
|
|
24
24
|
* and that failure already propagates.
|
|
25
25
|
* @public
|
|
26
26
|
*/
|
|
27
|
-
const STALE_LOCK_MAX_AGE_MS =
|
|
27
|
+
const STALE_LOCK_MAX_AGE_MS = 6e5;
|
|
28
28
|
/**
|
|
29
29
|
* Drives the vendored `.repos/` submodules over git: reports status
|
|
30
30
|
* (presence, dirtiness, stale notes), reconciles the working tree with the
|
|
@@ -32,252 +32,272 @@ const STALE_LOCK_MAX_AGE_MS = 10 * 6e4;
|
|
|
32
32
|
* ref (`pin`), and adds, removes, or promotes agent notes (`note`).
|
|
33
33
|
* @public
|
|
34
34
|
*/
|
|
35
|
-
var ReposManager = class extends Context.Service()("@savvy-web/silk-effects/ReposManager") {
|
|
36
|
-
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* @remarks
|
|
40
|
-
* All git plumbing runs through `@effected/git`'s `Git` service (captured once
|
|
41
|
-
* at layer construction; `Git.layer` requires `ChildProcessSpawner` at the app
|
|
42
|
-
* edge), so the public method effects stay at `R = never`. The kit classifies
|
|
43
|
-
* git's exit-code/stderr taxonomy into typed failures, which are mapped onto
|
|
44
|
-
* this module's {@link GitSubmoduleError} to keep the declared error unions.
|
|
45
|
-
* @public
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
35
|
+
var ReposManager = class extends Context.Service()("@savvy-web/silk-effects/ReposManager") {
|
|
36
|
+
/**
|
|
37
|
+
* Production implementation of {@link ReposManager}.
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* All git plumbing runs through `@effected/git`'s `Git` service (captured once
|
|
41
|
+
* at layer construction; `Git.layer` requires `ChildProcessSpawner` at the app
|
|
42
|
+
* edge), so the public method effects stay at `R = never`. The kit classifies
|
|
43
|
+
* git's exit-code/stderr taxonomy into typed failures, which are mapped onto
|
|
44
|
+
* this module's {@link GitSubmoduleError} to keep the declared error unions.
|
|
45
|
+
* @public
|
|
46
|
+
*/
|
|
47
|
+
static layer = Layer.effect(this, Effect.gen(function* () {
|
|
48
|
+
const configStore = yield* ReposConfigStore;
|
|
49
|
+
const fs = yield* FileSystem.FileSystem;
|
|
50
|
+
const path = yield* Path.Path;
|
|
51
|
+
const git = yield* Git;
|
|
52
|
+
/** Map any typed `@effected/git` failure onto this module's `GitSubmoduleError`. */
|
|
53
|
+
const asSubmoduleError = (command, cwd) => (error) => new GitSubmoduleError({
|
|
54
|
+
command,
|
|
55
|
+
cwd,
|
|
56
|
+
reason: error.message
|
|
57
|
+
});
|
|
58
|
+
const isPresent = (repoPath) => fs.readDirectory(repoPath).pipe(Effect.map((files) => files.length > 0), Effect.orElseSucceed(() => false));
|
|
59
|
+
const status = (root) => Effect.gen(function* () {
|
|
60
|
+
const manifest = yield* configStore.read(root);
|
|
61
|
+
const repos = yield* Effect.forEach(Object.entries(manifest.repos), ([name, entry]) => Effect.gen(function* () {
|
|
62
|
+
const repoPath = path.join(root, REPOS_DIR, name);
|
|
63
|
+
const present = yield* isPresent(repoPath);
|
|
64
|
+
const commit = (yield* git.lsTree(root, "HEAD", { pathspec: [`${".repos"}/${name}`] }).pipe(Effect.mapError(asSubmoduleError(`git ls-tree HEAD -- ${".repos"}/${name}`, root))))[0]?.oid ?? null;
|
|
65
|
+
let dirty = false;
|
|
66
|
+
if (present) dirty = (yield* git.status(repoPath).pipe(Effect.mapError(asSubmoduleError("git status --porcelain", repoPath)))).length > 0;
|
|
67
|
+
const staleNoteIds = (entry.notes ?? []).filter((note) => note.ref !== entry.ref).map((note) => note.id);
|
|
68
|
+
return {
|
|
69
|
+
name,
|
|
70
|
+
ref: entry.ref,
|
|
71
|
+
purpose: entry.purpose,
|
|
72
|
+
present,
|
|
73
|
+
commit,
|
|
74
|
+
dirty,
|
|
75
|
+
staleNoteIds
|
|
76
|
+
};
|
|
77
|
+
}));
|
|
68
78
|
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
purpose: entry.purpose,
|
|
72
|
-
present,
|
|
73
|
-
commit,
|
|
74
|
-
dirty,
|
|
75
|
-
staleNoteIds
|
|
79
|
+
repos,
|
|
80
|
+
clean: repos.every((entry) => entry.present && !entry.dirty && entry.staleNoteIds.length === 0)
|
|
76
81
|
};
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
} else upToDate.push(name);
|
|
114
|
-
if (entry.sparse && entry.sparse.length > 0) {
|
|
115
|
-
yield* git.sparseCheckoutSet(repoPath, entry.sparse, { cone: false }).pipe(Effect.mapError(asSubmoduleError("git sparse-checkout set --no-cone", repoPath)));
|
|
116
|
-
sparseApplied.push(name);
|
|
82
|
+
});
|
|
83
|
+
const sync = (root) => Effect.gen(function* () {
|
|
84
|
+
const manifest = yield* configStore.read(root);
|
|
85
|
+
const initialized = [];
|
|
86
|
+
const sparseApplied = [];
|
|
87
|
+
const upToDate = [];
|
|
88
|
+
const clearedLocks = [];
|
|
89
|
+
for (const [name, entry] of Object.entries(manifest.repos)) {
|
|
90
|
+
const repoPath = path.join(root, REPOS_DIR, name);
|
|
91
|
+
const moduleDir = path.join(root, ".git", "modules", REPOS_DIR, name);
|
|
92
|
+
let clearedAnyLock = false;
|
|
93
|
+
for (const lock of STALE_LOCKS) {
|
|
94
|
+
const lockPath = path.join(moduleDir, lock);
|
|
95
|
+
const info = yield* fs.stat(lockPath).pipe(Effect.option);
|
|
96
|
+
if (Option.isNone(info)) continue;
|
|
97
|
+
const mtime = info.value.mtime;
|
|
98
|
+
if (Option.isNone(mtime)) continue;
|
|
99
|
+
if ((yield* Clock.currentTimeMillis) - mtime.value.getTime() < 6e5) continue;
|
|
100
|
+
if (yield* fs.remove(lockPath).pipe(Effect.match({
|
|
101
|
+
onSuccess: () => true,
|
|
102
|
+
onFailure: () => false
|
|
103
|
+
}))) clearedAnyLock = true;
|
|
104
|
+
}
|
|
105
|
+
if (clearedAnyLock) clearedLocks.push(name);
|
|
106
|
+
if (!(yield* isPresent(repoPath))) {
|
|
107
|
+
yield* git.submoduleUpdate(root, {
|
|
108
|
+
init: true,
|
|
109
|
+
depth: 1,
|
|
110
|
+
paths: [`${REPOS_DIR}/${name}`]
|
|
111
|
+
}).pipe(Effect.mapError(asSubmoduleError(`git submodule update --init --depth 1 -- ${REPOS_DIR}/${name}`, root)));
|
|
112
|
+
initialized.push(name);
|
|
113
|
+
} else upToDate.push(name);
|
|
114
|
+
if (entry.sparse && entry.sparse.length > 0) {
|
|
115
|
+
yield* git.sparseCheckoutSet(repoPath, entry.sparse, { cone: false }).pipe(Effect.mapError(asSubmoduleError("git sparse-checkout set --no-cone", repoPath)));
|
|
116
|
+
sparseApplied.push(name);
|
|
117
|
+
}
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return last.endsWith(".git") ? last.slice(0, -4) : last;
|
|
130
|
-
};
|
|
131
|
-
/**
|
|
132
|
-
* Fetch `ref` shallow into a submodule via the kit's `Git.fetchAny`:
|
|
133
|
-
* tag-form fetch first (tags need the explicit `fetch origin tag <ref>`
|
|
134
|
-
* form), falling back to a plain `fetch origin <ref>` on
|
|
135
|
-
* `UnknownRefError` OR `GitCommandError` (unclassified tag-form stderr
|
|
136
|
-
* keeps the v3 any-failure fallback). `NotARepositoryError` deliberately
|
|
137
|
-
* does NOT fall back — a plain fetch in a non-repo fails identically, so
|
|
138
|
-
* retrying only doubled the latency of a certain failure. When both
|
|
139
|
-
* attempts fail, the plain fetch's error surfaces.
|
|
140
|
-
*/
|
|
141
|
-
const fetchRef = (sub, ref) => git.fetchAny(sub, {
|
|
142
|
-
ref,
|
|
143
|
-
depth: 1
|
|
144
|
-
}).pipe(Effect.mapError(asSubmoduleError(`git fetch --depth 1 origin ${ref}`, sub)));
|
|
145
|
-
const add = (root, options) => Effect.gen(function* () {
|
|
146
|
-
const name = options.name ?? repoSlug(options.url);
|
|
147
|
-
yield* Schema.decodeUnknownEffect(RepoName)(name).pipe(Effect.mapError(() => new ReposConfigError({
|
|
148
|
-
path: MANIFEST_PATH,
|
|
149
|
-
reason: `invalid repo name "${name}": must be non-empty, contain no "/" or "\\", and not be "." or ".."`,
|
|
150
|
-
kind: "invalid"
|
|
151
|
-
})));
|
|
152
|
-
const manifest = yield* configStore.read(root).pipe(Effect.catchTag("ReposConfigError", (error) => error.kind === "missing" ? Effect.succeed({ repos: {} }) : Effect.fail(error)));
|
|
153
|
-
if (manifest.repos[name]) return yield* Effect.fail(new ReposConfigError({
|
|
154
|
-
path: MANIFEST_PATH,
|
|
155
|
-
reason: `"${name}" is already vendored — use pin to change its ref`,
|
|
156
|
-
kind: "invalid"
|
|
157
|
-
}));
|
|
158
|
-
const repoPath = `${REPOS_DIR}/${name}`;
|
|
159
|
-
const subPath = path.join(root, repoPath);
|
|
160
|
-
yield* git.submoduleAdd(root, {
|
|
161
|
-
url: options.url,
|
|
162
|
-
path: repoPath,
|
|
163
|
-
depth: 1
|
|
164
|
-
}).pipe(Effect.mapError(asSubmoduleError(`git submodule add --depth 1 ${options.url} ${repoPath}`, root)));
|
|
165
|
-
yield* git.configSet(root, `submodule.${repoPath}.shallow`, "true", { file: ".gitmodules" }).pipe(Effect.mapError(asSubmoduleError(`git config -f .gitmodules submodule.${repoPath}.shallow true`, root)));
|
|
166
|
-
yield* fetchRef(subPath, options.ref);
|
|
167
|
-
yield* git.checkout(subPath, "FETCH_HEAD", { detach: true }).pipe(Effect.mapError(asSubmoduleError("git checkout --detach FETCH_HEAD", subPath)));
|
|
168
|
-
if (options.sparse && options.sparse.length > 0) yield* git.sparseCheckoutSet(subPath, options.sparse, { cone: false }).pipe(Effect.mapError(asSubmoduleError("git sparse-checkout set --no-cone", subPath)));
|
|
169
|
-
const entry = {
|
|
170
|
-
url: options.url,
|
|
171
|
-
ref: options.ref,
|
|
172
|
-
purpose: options.purpose,
|
|
173
|
-
...options.sparse && options.sparse.length > 0 ? { sparse: options.sparse } : {}
|
|
174
|
-
};
|
|
175
|
-
yield* configStore.write(root, { repos: {
|
|
176
|
-
...manifest.repos,
|
|
177
|
-
[name]: entry
|
|
178
|
-
} });
|
|
179
|
-
yield* git.add(root, [
|
|
180
|
-
".gitmodules",
|
|
181
|
-
MANIFEST_PATH,
|
|
182
|
-
repoPath
|
|
183
|
-
]).pipe(Effect.mapError(asSubmoduleError(`git add .gitmodules ${MANIFEST_PATH} ${repoPath}`, root)));
|
|
184
|
-
return {
|
|
185
|
-
name,
|
|
186
|
-
ref: options.ref,
|
|
187
|
-
path: repoPath
|
|
119
|
+
return {
|
|
120
|
+
initialized,
|
|
121
|
+
sparseApplied,
|
|
122
|
+
upToDate,
|
|
123
|
+
clearedLocks
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
/** Last path segment of a repo URL, with a trailing `.git` stripped. */
|
|
127
|
+
const repoSlug = (url) => {
|
|
128
|
+
const last = url.split("/").filter((segment) => segment.length > 0).pop() ?? url;
|
|
129
|
+
return last.endsWith(".git") ? last.slice(0, -4) : last;
|
|
188
130
|
};
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
yield* configStore.write(root, { repos: {
|
|
201
|
-
...manifest.repos,
|
|
202
|
-
[name]: {
|
|
203
|
-
...entry,
|
|
204
|
-
ref
|
|
205
|
-
}
|
|
206
|
-
} });
|
|
207
|
-
yield* git.add(root, [MANIFEST_PATH, repoPath]).pipe(Effect.mapError(asSubmoduleError(`git add ${MANIFEST_PATH} ${repoPath}`, root)));
|
|
208
|
-
const staleNoteIds = (entry.notes ?? []).filter((note) => note.ref !== ref).map((note) => note.id);
|
|
209
|
-
return {
|
|
210
|
-
name,
|
|
131
|
+
/**
|
|
132
|
+
* Fetch `ref` shallow into a submodule via the kit's `Git.fetchAny`:
|
|
133
|
+
* tag-form fetch first (tags need the explicit `fetch origin tag <ref>`
|
|
134
|
+
* form), falling back to a plain `fetch origin <ref>` on
|
|
135
|
+
* `UnknownRefError` OR `GitCommandError` (unclassified tag-form stderr
|
|
136
|
+
* keeps the v3 any-failure fallback). `NotARepositoryError` deliberately
|
|
137
|
+
* does NOT fall back — a plain fetch in a non-repo fails identically, so
|
|
138
|
+
* retrying only doubled the latency of a certain failure. When both
|
|
139
|
+
* attempts fail, the plain fetch's error surfaces.
|
|
140
|
+
*/
|
|
141
|
+
const fetchRef = (sub, ref) => git.fetchAny(sub, {
|
|
211
142
|
ref,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (op.op === "add") {
|
|
224
|
-
if (notes.length >= 10) return yield* Effect.fail(new ReposConfigError({
|
|
143
|
+
depth: 1
|
|
144
|
+
}).pipe(Effect.mapError(asSubmoduleError(`git fetch --depth 1 origin ${ref}`, sub)));
|
|
145
|
+
const add = (root, options) => Effect.gen(function* () {
|
|
146
|
+
const name = options.name ?? repoSlug(options.url);
|
|
147
|
+
yield* Schema.decodeUnknownEffect(RepoName)(name).pipe(Effect.mapError(() => new ReposConfigError({
|
|
148
|
+
path: MANIFEST_PATH,
|
|
149
|
+
reason: `invalid repo name "${name}": must be non-empty, contain no "/" or "\\", and not be "." or ".."`,
|
|
150
|
+
kind: "invalid"
|
|
151
|
+
})));
|
|
152
|
+
const manifest = yield* configStore.read(root).pipe(Effect.catchTag("ReposConfigError", (error) => error.kind === "missing" ? Effect.succeed({ repos: {} }) : Effect.fail(error)));
|
|
153
|
+
if (manifest.repos[name]) return yield* Effect.fail(new ReposConfigError({
|
|
225
154
|
path: MANIFEST_PATH,
|
|
226
|
-
reason: `
|
|
155
|
+
reason: `"${name}" is already vendored — use pin to change its ref`,
|
|
227
156
|
kind: "invalid"
|
|
228
157
|
}));
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
id = candidate;
|
|
247
|
-
}
|
|
248
|
-
const millis = yield* Clock.currentTimeMillis;
|
|
249
|
-
const date = new Date(millis).toISOString().slice(0, 10);
|
|
250
|
-
const newNote = {
|
|
251
|
-
id,
|
|
252
|
-
date,
|
|
253
|
-
ref: entry.ref,
|
|
254
|
-
note: op.note
|
|
158
|
+
const repoPath = `${REPOS_DIR}/${name}`;
|
|
159
|
+
const subPath = path.join(root, repoPath);
|
|
160
|
+
yield* git.submoduleAdd(root, {
|
|
161
|
+
url: options.url,
|
|
162
|
+
path: repoPath,
|
|
163
|
+
depth: 1
|
|
164
|
+
}).pipe(Effect.mapError(asSubmoduleError(`git submodule add --depth 1 ${options.url} ${repoPath}`, root)));
|
|
165
|
+
yield* git.configSet(root, `submodule.${repoPath}.shallow`, "true", { file: ".gitmodules" }).pipe(Effect.mapError(asSubmoduleError(`git config -f .gitmodules submodule.${repoPath}.shallow true`, root)));
|
|
166
|
+
yield* fetchRef(subPath, options.ref);
|
|
167
|
+
yield* git.checkout(subPath, "FETCH_HEAD", { detach: true }).pipe(Effect.mapError(asSubmoduleError("git checkout --detach FETCH_HEAD", subPath)));
|
|
168
|
+
if (options.sparse && options.sparse.length > 0) yield* git.sparseCheckoutSet(subPath, options.sparse, { cone: false }).pipe(Effect.mapError(asSubmoduleError("git sparse-checkout set --no-cone", subPath)));
|
|
169
|
+
const entry = {
|
|
170
|
+
url: options.url,
|
|
171
|
+
ref: options.ref,
|
|
172
|
+
purpose: options.purpose,
|
|
173
|
+
...options.sparse && options.sparse.length > 0 ? { sparse: options.sparse } : {}
|
|
255
174
|
};
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
175
|
+
yield* configStore.write(root, { repos: {
|
|
176
|
+
...manifest.repos,
|
|
177
|
+
[name]: entry
|
|
178
|
+
} });
|
|
179
|
+
yield* git.add(root, [
|
|
180
|
+
".gitmodules",
|
|
181
|
+
MANIFEST_PATH,
|
|
182
|
+
repoPath
|
|
183
|
+
]).pipe(Effect.mapError(asSubmoduleError(`git add .gitmodules ${MANIFEST_PATH} ${repoPath}`, root)));
|
|
184
|
+
return {
|
|
185
|
+
name,
|
|
186
|
+
ref: options.ref,
|
|
187
|
+
path: repoPath
|
|
260
188
|
};
|
|
189
|
+
});
|
|
190
|
+
const pin = (root, name, ref) => Effect.gen(function* () {
|
|
191
|
+
const manifest = yield* configStore.read(root);
|
|
192
|
+
const entry = manifest.repos[name];
|
|
193
|
+
if (!entry) return yield* Effect.fail(new RepoNotFoundError({ name }));
|
|
194
|
+
const repoPath = `${REPOS_DIR}/${name}`;
|
|
195
|
+
const subPath = path.join(root, repoPath);
|
|
196
|
+
const oldCommit = yield* git.revParse(subPath, "HEAD").pipe(Effect.orElseSucceed(() => null));
|
|
197
|
+
yield* fetchRef(subPath, ref);
|
|
198
|
+
yield* git.checkout(subPath, "FETCH_HEAD", { detach: true }).pipe(Effect.mapError(asSubmoduleError("git checkout --detach FETCH_HEAD", subPath)));
|
|
199
|
+
const newCommit = yield* git.revParse(subPath, "HEAD").pipe(Effect.mapError(asSubmoduleError("git rev-parse HEAD", subPath)));
|
|
261
200
|
yield* configStore.write(root, { repos: {
|
|
262
201
|
...manifest.repos,
|
|
263
|
-
[name]:
|
|
202
|
+
[name]: {
|
|
203
|
+
...entry,
|
|
204
|
+
ref
|
|
205
|
+
}
|
|
264
206
|
} });
|
|
207
|
+
yield* git.add(root, [MANIFEST_PATH, repoPath]).pipe(Effect.mapError(asSubmoduleError(`git add ${MANIFEST_PATH} ${repoPath}`, root)));
|
|
208
|
+
const staleNoteIds = (entry.notes ?? []).filter((note) => note.ref !== ref).map((note) => note.id);
|
|
265
209
|
return {
|
|
266
210
|
name,
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
211
|
+
ref,
|
|
212
|
+
oldCommit,
|
|
213
|
+
newCommit,
|
|
214
|
+
commitMessage: `chore(repos): pin ${name} to ${ref}`,
|
|
215
|
+
staleNoteIds
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
const note = (root, name, op) => Effect.gen(function* () {
|
|
219
|
+
const manifest = yield* configStore.read(root);
|
|
220
|
+
const entry = manifest.repos[name];
|
|
221
|
+
if (!entry) return yield* Effect.fail(new RepoNotFoundError({ name }));
|
|
222
|
+
const notes = entry.notes ?? [];
|
|
223
|
+
if (op.op === "add") {
|
|
224
|
+
if (notes.length >= 10) return yield* Effect.fail(new ReposConfigError({
|
|
225
|
+
path: MANIFEST_PATH,
|
|
226
|
+
reason: `note limit (${10}) reached for ${name}; promote or remove notes first`,
|
|
227
|
+
kind: "invalid"
|
|
228
|
+
}));
|
|
229
|
+
const existingIds = new Set(notes.map((existing) => existing.id));
|
|
230
|
+
const hash = createHash("sha256").update(op.note).digest("hex");
|
|
231
|
+
let id;
|
|
232
|
+
for (let len = 4; len <= hash.length; len += 4) {
|
|
233
|
+
const candidate = `n-${hash.slice(0, len)}`;
|
|
234
|
+
if (!existingIds.has(candidate)) {
|
|
235
|
+
id = candidate;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (id === void 0) {
|
|
240
|
+
let counter = 2;
|
|
241
|
+
let candidate = `n-${hash}-${counter}`;
|
|
242
|
+
while (existingIds.has(candidate)) {
|
|
243
|
+
counter += 1;
|
|
244
|
+
candidate = `n-${hash}-${counter}`;
|
|
245
|
+
}
|
|
246
|
+
id = candidate;
|
|
247
|
+
}
|
|
248
|
+
const millis = yield* Clock.currentTimeMillis;
|
|
249
|
+
const date = new Date(millis).toISOString().slice(0, 10);
|
|
250
|
+
const newNote = {
|
|
251
|
+
id,
|
|
252
|
+
date,
|
|
253
|
+
ref: entry.ref,
|
|
254
|
+
note: op.note
|
|
255
|
+
};
|
|
256
|
+
const updatedNotes = [...notes, newNote];
|
|
257
|
+
const updatedEntry = {
|
|
258
|
+
...entry,
|
|
259
|
+
notes: updatedNotes
|
|
260
|
+
};
|
|
261
|
+
yield* configStore.write(root, { repos: {
|
|
262
|
+
...manifest.repos,
|
|
263
|
+
[name]: updatedEntry
|
|
264
|
+
} });
|
|
265
|
+
return {
|
|
266
|
+
name,
|
|
267
|
+
op: "add",
|
|
268
|
+
id,
|
|
269
|
+
noteCount: updatedNotes.length
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const target = notes.find((existing) => existing.id === op.id);
|
|
273
|
+
if (!target) return yield* Effect.fail(new NoteNotFoundError({
|
|
274
|
+
name,
|
|
275
|
+
id: op.id
|
|
276
|
+
}));
|
|
277
|
+
const updatedNotes = notes.filter((existing) => existing.id !== op.id);
|
|
278
|
+
if (op.op === "remove") {
|
|
279
|
+
const updatedEntry = {
|
|
280
|
+
...entry,
|
|
281
|
+
notes: updatedNotes
|
|
282
|
+
};
|
|
283
|
+
yield* configStore.write(root, { repos: {
|
|
284
|
+
...manifest.repos,
|
|
285
|
+
[name]: updatedEntry
|
|
286
|
+
} });
|
|
287
|
+
return {
|
|
288
|
+
name,
|
|
289
|
+
op: "remove",
|
|
290
|
+
id: op.id,
|
|
291
|
+
noteCount: updatedNotes.length
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const updatedOrientation = {
|
|
295
|
+
...entry.orientation,
|
|
296
|
+
[op.into]: target.note
|
|
270
297
|
};
|
|
271
|
-
}
|
|
272
|
-
const target = notes.find((existing) => existing.id === op.id);
|
|
273
|
-
if (!target) return yield* Effect.fail(new NoteNotFoundError({
|
|
274
|
-
name,
|
|
275
|
-
id: op.id
|
|
276
|
-
}));
|
|
277
|
-
const updatedNotes = notes.filter((existing) => existing.id !== op.id);
|
|
278
|
-
if (op.op === "remove") {
|
|
279
298
|
const updatedEntry = {
|
|
280
299
|
...entry,
|
|
300
|
+
orientation: updatedOrientation,
|
|
281
301
|
notes: updatedNotes
|
|
282
302
|
};
|
|
283
303
|
yield* configStore.write(root, { repos: {
|
|
@@ -286,39 +306,20 @@ const ReposManagerLive = Layer.effect(ReposManager, Effect.gen(function* () {
|
|
|
286
306
|
} });
|
|
287
307
|
return {
|
|
288
308
|
name,
|
|
289
|
-
op: "
|
|
309
|
+
op: "promote",
|
|
290
310
|
id: op.id,
|
|
291
311
|
noteCount: updatedNotes.length
|
|
292
312
|
};
|
|
293
|
-
}
|
|
294
|
-
const updatedOrientation = {
|
|
295
|
-
...entry.orientation,
|
|
296
|
-
[op.into]: target.note
|
|
297
|
-
};
|
|
298
|
-
const updatedEntry = {
|
|
299
|
-
...entry,
|
|
300
|
-
orientation: updatedOrientation,
|
|
301
|
-
notes: updatedNotes
|
|
302
|
-
};
|
|
303
|
-
yield* configStore.write(root, { repos: {
|
|
304
|
-
...manifest.repos,
|
|
305
|
-
[name]: updatedEntry
|
|
306
|
-
} });
|
|
313
|
+
});
|
|
307
314
|
return {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
315
|
+
status,
|
|
316
|
+
sync,
|
|
317
|
+
add,
|
|
318
|
+
pin,
|
|
319
|
+
note
|
|
312
320
|
};
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
status,
|
|
316
|
-
sync,
|
|
317
|
-
add,
|
|
318
|
-
pin,
|
|
319
|
-
note
|
|
320
|
-
};
|
|
321
|
-
}));
|
|
321
|
+
}));
|
|
322
|
+
};
|
|
322
323
|
|
|
323
324
|
//#endregion
|
|
324
|
-
export { ReposManager,
|
|
325
|
+
export { ReposManager, STALE_LOCK_MAX_AGE_MS };
|