@savvy-web/silk-effects 3.3.1 → 4.0.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/changelog/getReleaseLine.js +1 -1
- package/changesets/constants.js +0 -18
- package/changesets/index.js +10 -24
- package/changesets/schemas/changeset.js +8 -4
- package/changesets/schemas/dependency-table.js +15 -4
- package/changesets/schemas/git.js +7 -2
- package/changesets/schemas/github.js +4 -4
- package/changesets/schemas/options.js +3 -3
- package/changesets/schemas/package-scope.js +2 -5
- package/changesets/schemas/primitives.js +2 -2
- package/changesets/schemas/release-plan.js +12 -8
- package/changesets/schemas/version-files.js +4 -4
- package/changesets/services/branch-analyzer.js +60 -191
- package/changesets/services/changelog.js +2 -15
- package/changesets/services/config-inspector.js +109 -75
- package/changesets/services/deps-regen.js +58 -32
- package/changesets/services/github.js +3 -16
- package/changesets/services/maintenance-reason.js +5 -1
- package/changesets/services/markdown.js +3 -16
- package/changesets/services/release-planner.js +4 -13
- package/changesets/utils/commit-parser.js +0 -18
- package/changesets/utils/dep-diff.js +0 -27
- package/changesets/utils/git.js +15 -50
- package/changesets/utils/issue-refs.js +0 -13
- package/changesets/utils/logger.js +0 -13
- package/changesets/utils/markdown-link.js +0 -14
- package/changesets/utils/publishability.js +11 -24
- package/changesets/utils/strip-frontmatter.js +0 -19
- package/changesets/utils/version-files.js +85 -51
- package/commitlint/config/schema.js +9 -5
- package/commitlint/detection/scopes.js +2 -7
- package/commitlint/formatter/messages.js +0 -5
- package/commitlint/hook/diagnostics/branch.js +12 -12
- package/commitlint/hook/diagnostics/open-issues.js +13 -7
- package/commitlint/hook/diagnostics/signing.js +30 -37
- package/commitlint/hook/envelope.js +2 -8
- package/commitlint/hook/silence-logger.js +14 -12
- package/index.d.ts +897 -932
- package/lint/cli/templates/markdownlint.gen.js +0 -9
- package/lint/handlers/PnpmWorkspace.js +26 -12
- package/lint/utils/Filter.js +0 -13
- package/lint/utils/Workspace.js +10 -6
- package/package.json +11 -10
- package/repos/index.js +3 -5
- package/repos/schemas/manifest.js +7 -13
- package/repos/schemas/reports.js +5 -1
- package/repos/services/config-store.js +6 -10
- package/repos/services/manager.js +52 -112
- package/schemas/CommentStyle.js +1 -1
- package/schemas/ResolvedTool.js +52 -17
- package/schemas/SectionBlock.js +1 -1
- package/schemas/SectionDefinition.js +2 -2
- package/schemas/TagStrategySchemas.js +1 -1
- package/schemas/ToolDefinition.js +1 -1
- package/schemas/ToolResults.js +1 -1
- package/schemas/VersioningSchemas.js +28 -9
- package/schemas/WorkspaceAnalysisSchemas.js +27 -17
- package/services/BiomeSchemaSync.js +7 -8
- package/services/ChangesetConfig.js +2 -2
- package/services/ChangesetConfigReader.js +7 -8
- package/services/ConfigDiscovery.js +5 -6
- package/services/ManagedSection.js +3 -4
- package/services/SilkPublishability.js +38 -15
- package/services/SilkWorkspaceAnalyzer.js +7 -9
- package/services/TagStrategy.js +3 -3
- package/services/ToolDiscovery.js +22 -21
- package/services/VersioningStrategy.js +2 -2
- package/tsdoc-metadata.json +1 -1
- package/turbo/schemas/DryRun.js +8 -14
- package/turbo/schemas/results.js +8 -8
- package/turbo/services/TurboInspector.js +30 -23
- package/utils/ToolCommand.js +38 -12
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { LegacyVersionFilesSchema } from "../schemas/version-files.js";
|
|
2
2
|
import { jsonPathGet, jsonPathResolve, parseJsonPath } from "./jsonpath.js";
|
|
3
|
-
import { Effect, Schema } from "effect";
|
|
3
|
+
import { Effect, Option, Path, Schema } from "effect";
|
|
4
4
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join, relative, resolve } from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { GlobPattern } from "@effected/glob";
|
|
7
|
+
import { descend } from "@effected/walker";
|
|
8
|
+
import { Jsonc, JsoncEdit, JsoncFormattingOptions, JsoncModifier } from "@effected/jsonc";
|
|
8
9
|
|
|
9
10
|
//#region src/changesets/utils/version-files.ts
|
|
10
11
|
/**
|
|
@@ -42,13 +43,14 @@ import { applyEdits, modify, parse } from "jsonc-effect";
|
|
|
42
43
|
*
|
|
43
44
|
* @example
|
|
44
45
|
* ```typescript
|
|
46
|
+
* import { Effect } from "effect";
|
|
45
47
|
* import { VersionFiles } from "../utils/version-files.js";
|
|
46
48
|
*
|
|
47
49
|
* const configs = VersionFiles.extractVersionFiles(parsedConfig);
|
|
48
50
|
* if (configs) {
|
|
49
|
-
* const updates = VersionFiles.processVersionFiles("/path/to/project", configs);
|
|
51
|
+
* const updates = yield* VersionFiles.processVersionFiles("/path/to/project", configs);
|
|
50
52
|
* for (const update of updates) {
|
|
51
|
-
*
|
|
53
|
+
* yield* Effect.log(`Updated ${update.filePath} to ${update.version}`);
|
|
52
54
|
* }
|
|
53
55
|
* }
|
|
54
56
|
* ```
|
|
@@ -159,25 +161,24 @@ var VersionFiles = class VersionFiles {
|
|
|
159
161
|
* Resolve glob patterns to absolute file paths.
|
|
160
162
|
*
|
|
161
163
|
* @remarks
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
164
|
+
* Compiles each config's glob with `@effected/glob` and expands it against
|
|
165
|
+
* a filesystem walk rooted at `cwd` via `@effected/walker`'s `descend`
|
|
166
|
+
* (literal patterns fast-path a direct stat; magic patterns walk from
|
|
167
|
+
* their literal prefix). The `node_modules` and `.git` directories are
|
|
168
|
+
* always ignored. Returns tuples pairing each resolved absolute path with
|
|
169
|
+
* its originating config, per-glob results sorted by relative path.
|
|
165
170
|
*
|
|
166
171
|
* @param configs - Version file configurations
|
|
167
172
|
* @param cwd - Project root directory
|
|
168
|
-
* @returns
|
|
173
|
+
* @returns Effect of `[filePath, config]` tuples
|
|
169
174
|
*/
|
|
170
175
|
static resolveGlobs(configs, cwd) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
});
|
|
178
|
-
for (const match of matches) results.push([join(resolvedCwd, match), config]);
|
|
179
|
-
}
|
|
180
|
-
return results;
|
|
176
|
+
return Effect.gen(function* () {
|
|
177
|
+
const results = [];
|
|
178
|
+
const resolvedCwd = resolve(cwd);
|
|
179
|
+
for (const config of configs) for (const match of yield* expandGlob(config.glob, resolvedCwd)) results.push([join(resolvedCwd, match), config]);
|
|
180
|
+
return results;
|
|
181
|
+
});
|
|
181
182
|
}
|
|
182
183
|
/**
|
|
183
184
|
* Detect indentation from file content.
|
|
@@ -252,7 +253,7 @@ var VersionFiles = class VersionFiles {
|
|
|
252
253
|
*/
|
|
253
254
|
static computeUpdate(original, jsonPaths, version) {
|
|
254
255
|
let content = original;
|
|
255
|
-
const obj = Effect.runSync(parse(content));
|
|
256
|
+
const obj = Effect.runSync(Jsonc.parse(content));
|
|
256
257
|
const previousValues = jsonPaths.flatMap((jp) => jsonPathGet(obj, jp));
|
|
257
258
|
const indent = VersionFiles.detectIndent(content);
|
|
258
259
|
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -286,9 +287,9 @@ var VersionFiles = class VersionFiles {
|
|
|
286
287
|
* the updated document, or `undefined` when nothing changed.
|
|
287
288
|
*
|
|
288
289
|
* @remarks
|
|
289
|
-
* Delegates to
|
|
290
|
-
*
|
|
291
|
-
*
|
|
290
|
+
* Delegates to `@effected/jsonc`'s `JsoncModifier.modify` +
|
|
291
|
+
* `JsoncEdit.applyAll` (whose edit spans touch only the target value), so
|
|
292
|
+
* every other byte of the document is preserved. When the leaf
|
|
292
293
|
* of a wildcard-free path does not exist, `modify` inserts the property
|
|
293
294
|
* after the last sibling using the supplied formatting options — the only
|
|
294
295
|
* case where the detected indent matters. A path whose parent is missing or
|
|
@@ -304,11 +305,11 @@ var VersionFiles = class VersionFiles {
|
|
|
304
305
|
*/
|
|
305
306
|
static applyVersionEdit(content, concretePath, version, indentUnit, eol) {
|
|
306
307
|
const insertSpaces = !indentUnit.includes(" ");
|
|
307
|
-
const program = modify(content, [...concretePath], version, { formattingOptions: {
|
|
308
|
+
const program = JsoncModifier.modify(content, [...concretePath], version, { formattingOptions: JsoncFormattingOptions.make({
|
|
308
309
|
insertSpaces,
|
|
309
310
|
tabSize: insertSpaces ? indentUnit.length : 1,
|
|
310
311
|
eol
|
|
311
|
-
} }).pipe(Effect.
|
|
312
|
+
}) }).pipe(Effect.map((edits) => JsoncEdit.applyAll(content, edits)), Effect.map((updated) => updated === content ? void 0 : updated), Effect.catchTag("JsoncModificationError", () => Effect.succeed(void 0)));
|
|
312
313
|
return Effect.runSync(program);
|
|
313
314
|
}
|
|
314
315
|
/**
|
|
@@ -323,36 +324,39 @@ var VersionFiles = class VersionFiles {
|
|
|
323
324
|
* @param cwd - Project root directory
|
|
324
325
|
* @param configs - Validated version file configurations
|
|
325
326
|
* @param dryRun - If true, do not write files (default: `false`)
|
|
326
|
-
* @returns
|
|
327
|
-
*
|
|
327
|
+
* @returns Effect of update results; a file that cannot be read or parsed
|
|
328
|
+
* is a defect (the legacy path treats it as a caller bug, as the
|
|
329
|
+
* previous synchronous `throw` did)
|
|
328
330
|
*/
|
|
329
331
|
static processVersionFiles(cwd, configs, dryRun = false, packages = []) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
332
|
+
return Effect.gen(function* () {
|
|
333
|
+
const workspaces = VersionFiles.discoverVersions(cwd, packages);
|
|
334
|
+
const rootVersion = workspaces.find((ws) => ws.path === resolve(cwd))?.version ?? "0.0.0";
|
|
335
|
+
const resolved = yield* VersionFiles.resolveGlobs(configs, cwd);
|
|
336
|
+
const updates = [];
|
|
337
|
+
for (const [filePath, config] of resolved) {
|
|
338
|
+
const jsonPaths = config.paths ?? ["$.version"];
|
|
339
|
+
const version = config.package ? workspaces.find((ws) => ws.name === config.package)?.version ?? rootVersion : VersionFiles.resolveVersion(filePath, workspaces, rootVersion);
|
|
340
|
+
try {
|
|
341
|
+
if (dryRun) {
|
|
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)}`);
|
|
350
356
|
}
|
|
351
|
-
} catch (error) {
|
|
352
|
-
throw new Error(`Failed to update ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
353
357
|
}
|
|
354
|
-
|
|
355
|
-
|
|
358
|
+
return updates;
|
|
359
|
+
});
|
|
356
360
|
}
|
|
357
361
|
/**
|
|
358
362
|
* Apply version-file updates from the resolved (post-`ConfigInspector`)
|
|
@@ -401,6 +405,36 @@ var VersionFiles = class VersionFiles {
|
|
|
401
405
|
}
|
|
402
406
|
};
|
|
403
407
|
/**
|
|
408
|
+
* Expand a glob pattern against the filesystem, returning matching FILE paths
|
|
409
|
+
* relative to `cwd` (POSIX separators), sorted by relative path.
|
|
410
|
+
*
|
|
411
|
+
* @remarks
|
|
412
|
+
* The walk is `@effected/walker`'s `descend` (literal fast path,
|
|
413
|
+
* `enumerationPrefix` bounding, `node_modules`/`.git` pruning), with
|
|
414
|
+
* `onUnreadable: "skip"` preserving this module's silent-skip policy for
|
|
415
|
+
* unreadable directories. Matching semantics are minimatch DEFAULTS via the
|
|
416
|
+
* compiled pattern — notably, wildcards do not match dotfiles, same as the
|
|
417
|
+
* previous `tinyglobby` defaults. An uncompilable pattern expands to no
|
|
418
|
+
* matches. `descend`'s `Path` requirement is satisfied internally with the
|
|
419
|
+
* core POSIX `Path.layer` — this module already speaks POSIX relative match
|
|
420
|
+
* paths and node-bound absolute paths throughout.
|
|
421
|
+
*
|
|
422
|
+
* @param source - The glob pattern, repo-relative
|
|
423
|
+
* @param cwd - Absolute directory the pattern is resolved against
|
|
424
|
+
* @returns Effect of relative paths of matching files, sorted
|
|
425
|
+
*
|
|
426
|
+
* @internal
|
|
427
|
+
*/
|
|
428
|
+
function expandGlob(source, cwd) {
|
|
429
|
+
return Effect.option(GlobPattern.compile(source)).pipe(Effect.flatMap(Option.match({
|
|
430
|
+
onNone: () => Effect.succeed([]),
|
|
431
|
+
onSome: (pattern) => descend(pattern, {
|
|
432
|
+
cwd,
|
|
433
|
+
onUnreadable: "skip"
|
|
434
|
+
})
|
|
435
|
+
})), Effect.provide(Path.layer));
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
404
438
|
* Read the `version` field from a `package.json` in the given directory.
|
|
405
439
|
*
|
|
406
440
|
* @param dir - Absolute path to the directory containing `package.json`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
2
|
|
|
3
3
|
//#region src/commitlint/config/schema.ts
|
|
4
4
|
/**
|
|
@@ -11,7 +11,11 @@ import { Schema } from "effect";
|
|
|
11
11
|
*
|
|
12
12
|
* @internal
|
|
13
13
|
*/
|
|
14
|
-
const ReleaseFormatSchema = Schema.
|
|
14
|
+
const ReleaseFormatSchema = Schema.Literals([
|
|
15
|
+
"semver",
|
|
16
|
+
"packages",
|
|
17
|
+
"scoped"
|
|
18
|
+
]);
|
|
15
19
|
/**
|
|
16
20
|
* Effect Schema for validating {@link ConfigOptions}.
|
|
17
21
|
*
|
|
@@ -26,9 +30,9 @@ const ConfigOptionsSchema = Schema.Struct({
|
|
|
26
30
|
scopes: Schema.optional(Schema.Array(Schema.String)),
|
|
27
31
|
additionalScopes: Schema.optional(Schema.Array(Schema.String)),
|
|
28
32
|
releaseFormat: Schema.optional(ReleaseFormatSchema),
|
|
29
|
-
emojis: Schema.
|
|
30
|
-
bodyMaxLineLength: Schema.
|
|
31
|
-
noMarkdown: Schema.
|
|
33
|
+
emojis: Schema.Boolean.pipe(Schema.withDecodingDefaultType(Effect.succeed(false))),
|
|
34
|
+
bodyMaxLineLength: Schema.Number.check(Schema.isGreaterThan(0)).pipe(Schema.withDecodingDefaultType(Effect.succeed(300))),
|
|
35
|
+
noMarkdown: Schema.Boolean.pipe(Schema.withDecodingDefaultType(Effect.succeed(true))),
|
|
32
36
|
cwd: Schema.optional(Schema.String)
|
|
33
37
|
});
|
|
34
38
|
/**
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import { WorkspaceDiscovery } from "workspaces
|
|
2
|
+
import { WorkspaceDiscovery } from "@effected/workspaces";
|
|
3
3
|
|
|
4
4
|
//#region src/commitlint/detection/scopes.ts
|
|
5
5
|
/**
|
|
6
|
-
* Workspace scope detection module.
|
|
7
|
-
*
|
|
8
|
-
* @internal
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
6
|
* Extract scope-friendly name from a package name.
|
|
12
7
|
*
|
|
13
8
|
* @param name - Package name (e.g., `@scope/package-name` or `package-name`)
|
|
@@ -23,7 +18,7 @@ function extractScopeName(name) {
|
|
|
23
18
|
* Detect package scopes from workspace configuration.
|
|
24
19
|
*
|
|
25
20
|
* @remarks
|
|
26
|
-
* Uses workspaces
|
|
21
|
+
* Uses `@effected/workspaces` to find all packages in the workspace and extracts
|
|
27
22
|
* their names as potential commit scopes. For scoped packages like
|
|
28
23
|
* `@scope/package-name`, only the package name portion is used as the scope.
|
|
29
24
|
*
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import {
|
|
3
|
-
import { promisify } from "node:util";
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { Git } from "@effected/git";
|
|
4
3
|
|
|
5
4
|
//#region src/commitlint/hook/diagnostics/branch.ts
|
|
6
5
|
/**
|
|
7
6
|
* Branch and inferred-ticket detection.
|
|
8
7
|
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Branch lookup runs through `@effected/git`'s `currentBranch` (Option-shaped:
|
|
10
|
+
* a detached `HEAD` is `Option.none`, not the literal string `"HEAD"`). Any
|
|
11
|
+
* git failure degrades to the `{ branch: null }` fallback, preserving the
|
|
12
|
+
* never-fails contract of the v3 implementation.
|
|
13
|
+
*
|
|
9
14
|
* @internal
|
|
10
15
|
*/
|
|
11
|
-
const execFileP = promisify(execFile);
|
|
12
16
|
const TICKET_RE = /^[a-z]+\/(\d+)[-/_]/;
|
|
13
17
|
function inferTicketId(branch) {
|
|
14
18
|
const m = branch.match(TICKET_RE);
|
|
15
19
|
return m ? Number(m[1]) : null;
|
|
16
20
|
}
|
|
17
21
|
function readBranchInfo() {
|
|
18
|
-
return Effect.
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
"--abbrev-ref",
|
|
22
|
-
"HEAD"
|
|
23
|
-
]);
|
|
24
|
-
const branch = stdout.trim();
|
|
22
|
+
return Effect.gen(function* () {
|
|
23
|
+
const branchOption = yield* (yield* Git).currentBranch(process.cwd());
|
|
24
|
+
const branch = Option.getOrNull(branchOption);
|
|
25
25
|
return {
|
|
26
26
|
branch,
|
|
27
|
-
inferredTicketId: inferTicketId(branch)
|
|
27
|
+
inferredTicketId: branch === null ? null : inferTicketId(branch)
|
|
28
28
|
};
|
|
29
29
|
}).pipe(Effect.orElseSucceed(() => ({
|
|
30
30
|
branch: null,
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { readCache, writeCache } from "./cache.js";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
|
-
import {
|
|
4
|
-
import { promisify } from "node:util";
|
|
3
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
5
4
|
|
|
6
5
|
//#region src/commitlint/hook/diagnostics/open-issues.ts
|
|
7
6
|
/**
|
|
8
7
|
* Open-issue lookup via gh CLI, cached on disk.
|
|
9
8
|
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The `gh` invocations are not git, so they stay hand-rolled — the spawn
|
|
11
|
+
* mechanism moved from promisified `node:child_process.execFile` onto
|
|
12
|
+
* `effect/unstable/process` `ChildProcess`. Any failure (gh missing, not
|
|
13
|
+
* logged in, no repo, malformed JSON) degrades to `null`, preserving the
|
|
14
|
+
* never-fails contract of the v3 implementation.
|
|
15
|
+
*
|
|
10
16
|
* @internal
|
|
11
17
|
*/
|
|
12
|
-
const execFileP = promisify(execFile);
|
|
13
18
|
const ISSUES_CACHE_TTL_SECONDS = 600;
|
|
14
19
|
/** Relative path under CLAUDE_PROJECT_DIR where the open-issues cache lives. */
|
|
15
20
|
const ISSUES_CACHE_RELATIVE_PATH = ".claude/cache/issues.json";
|
|
@@ -18,16 +23,17 @@ function readOpenIssuesFromCache(cachePath, ttlSeconds = 600) {
|
|
|
18
23
|
}
|
|
19
24
|
function fetchAndCacheOpenIssues(cachePath) {
|
|
20
25
|
return Effect.gen(function* () {
|
|
21
|
-
const
|
|
26
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
27
|
+
const repo = (yield* spawner.string(ChildProcess.make("gh", [
|
|
22
28
|
"repo",
|
|
23
29
|
"view",
|
|
24
30
|
"--json",
|
|
25
31
|
"nameWithOwner",
|
|
26
32
|
"-q",
|
|
27
33
|
".nameWithOwner"
|
|
28
|
-
]))).
|
|
34
|
+
]))).trim();
|
|
29
35
|
if (!repo) return null;
|
|
30
|
-
const
|
|
36
|
+
const listStdout = yield* spawner.string(ChildProcess.make("gh", [
|
|
31
37
|
"issue",
|
|
32
38
|
"list",
|
|
33
39
|
"--repo",
|
|
@@ -39,7 +45,7 @@ function fetchAndCacheOpenIssues(cachePath) {
|
|
|
39
45
|
"--json",
|
|
40
46
|
"number,title"
|
|
41
47
|
]));
|
|
42
|
-
const parsed = JSON.parse(
|
|
48
|
+
const parsed = yield* Effect.try(() => JSON.parse(listStdout));
|
|
43
49
|
yield* writeCache(cachePath, parsed);
|
|
44
50
|
return parsed;
|
|
45
51
|
}).pipe(Effect.orElseSucceed(() => null));
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import {
|
|
3
|
-
import { promisify } from "node:util";
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { Git } from "@effected/git";
|
|
4
3
|
import { stat } from "node:fs/promises";
|
|
4
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
5
5
|
|
|
6
6
|
//#region src/commitlint/hook/diagnostics/signing.ts
|
|
7
7
|
/**
|
|
8
8
|
* GPG / SSH signing diagnostic.
|
|
9
9
|
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* The `git config` reads run through `@effected/git`'s `configGet`
|
|
12
|
+
* (Option-shaped: an unset key is `Option.none`). The `gpg` /
|
|
13
|
+
* `gpg-connect-agent` probes are not git, so they stay hand-rolled on
|
|
14
|
+
* `effect/unstable/process` `ChildProcess`; every probe degrades to its v3
|
|
15
|
+
* fallback value (`keyResolves: false`, `agentResponsive: false`) on any
|
|
16
|
+
* failure, and the diagnostic as a whole degrades to
|
|
17
|
+
* {@link FALLBACK_DIAGNOSTIC}, preserving the never-fails contract.
|
|
18
|
+
*
|
|
10
19
|
* @internal
|
|
11
20
|
*/
|
|
12
|
-
const execFileP = promisify(execFile);
|
|
13
21
|
function parseGpgKeyExpiry(colonsOutput) {
|
|
14
22
|
for (const line of colonsOutput.split("\n")) {
|
|
15
23
|
if (!line.startsWith("sec:")) continue;
|
|
@@ -38,19 +46,6 @@ function buildSigningDiagnostic(raw) {
|
|
|
38
46
|
warnings
|
|
39
47
|
};
|
|
40
48
|
}
|
|
41
|
-
async function gitConfig(key) {
|
|
42
|
-
try {
|
|
43
|
-
const { stdout } = await execFileP("git", [
|
|
44
|
-
"config",
|
|
45
|
-
"--get",
|
|
46
|
-
key
|
|
47
|
-
]);
|
|
48
|
-
const v = stdout.trim();
|
|
49
|
-
return v.length > 0 ? v : null;
|
|
50
|
-
} catch {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
49
|
const FALLBACK_DIAGNOSTIC = {
|
|
55
50
|
format: "none",
|
|
56
51
|
autoSignEnabled: false,
|
|
@@ -59,38 +54,36 @@ const FALLBACK_DIAGNOSTIC = {
|
|
|
59
54
|
agentResponsive: false,
|
|
60
55
|
warnings: ["signing diagnostic unavailable"]
|
|
61
56
|
};
|
|
57
|
+
/** `git config --get <key>`, degraded to null when unset or unreadable. */
|
|
58
|
+
const gitConfig = (key) => Effect.gen(function* () {
|
|
59
|
+
const value = yield* (yield* Git).configGet(process.cwd(), key);
|
|
60
|
+
return Option.getOrNull(value);
|
|
61
|
+
}).pipe(Effect.orElseSucceed(() => null));
|
|
62
62
|
function readSigningDiagnostic() {
|
|
63
|
-
return Effect.
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
63
|
+
return Effect.gen(function* () {
|
|
64
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
65
|
+
const gpgFormat = yield* gitConfig("gpg.format");
|
|
66
|
+
const commitGpgsign = yield* gitConfig("commit.gpgsign");
|
|
67
|
+
const signingKey = yield* gitConfig("user.signingkey");
|
|
68
|
+
const sshAllowedSignersFile = yield* gitConfig("gpg.ssh.allowedSignersFile");
|
|
68
69
|
const isSsh = gpgFormat === "ssh";
|
|
69
70
|
let keyResolves = false;
|
|
70
71
|
let keyExpiry = null;
|
|
71
|
-
if (signingKey) if (isSsh)
|
|
72
|
+
if (signingKey) if (isSsh) keyResolves = yield* Effect.tryPromise(async () => {
|
|
72
73
|
await stat(signingKey);
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
else try {
|
|
78
|
-
const { stdout } = await execFileP("gpg", [
|
|
74
|
+
return true;
|
|
75
|
+
}).pipe(Effect.orElseSucceed(() => false));
|
|
76
|
+
else {
|
|
77
|
+
const stdout = yield* spawner.string(ChildProcess.make("gpg", [
|
|
79
78
|
"--list-secret-keys",
|
|
80
79
|
"--with-colons",
|
|
81
80
|
signingKey
|
|
82
|
-
]);
|
|
81
|
+
])).pipe(Effect.orElseSucceed(() => ""));
|
|
83
82
|
keyResolves = stdout.trim().length > 0;
|
|
84
83
|
keyExpiry = parseGpgKeyExpiry(stdout);
|
|
85
|
-
} catch {
|
|
86
|
-
keyResolves = false;
|
|
87
84
|
}
|
|
88
85
|
let agentResponsive = true;
|
|
89
|
-
if (!isSsh)
|
|
90
|
-
await execFileP("gpg-connect-agent", ["/bye"], { timeout: 1e3 });
|
|
91
|
-
} catch {
|
|
92
|
-
agentResponsive = false;
|
|
93
|
-
}
|
|
86
|
+
if (!isSsh) agentResponsive = yield* spawner.exitCode(ChildProcess.make("gpg-connect-agent", ["/bye"])).pipe(Effect.timeout("1 second"), Effect.map((code) => code === 0), Effect.orElseSucceed(() => false));
|
|
94
87
|
return buildSigningDiagnostic({
|
|
95
88
|
gpgFormat,
|
|
96
89
|
commitGpgsign,
|
|
@@ -14,18 +14,12 @@ import { Schema } from "effect";
|
|
|
14
14
|
const PreToolUseEnvelope = Schema.Struct({
|
|
15
15
|
hook_event_name: Schema.Literal("PreToolUse"),
|
|
16
16
|
tool_name: Schema.String,
|
|
17
|
-
tool_input: Schema.Record(
|
|
18
|
-
key: Schema.String,
|
|
19
|
-
value: Schema.Unknown
|
|
20
|
-
})
|
|
17
|
+
tool_input: Schema.Record(Schema.String, Schema.Unknown)
|
|
21
18
|
});
|
|
22
19
|
const PostToolUseEnvelope = Schema.Struct({
|
|
23
20
|
hook_event_name: Schema.Literal("PostToolUse"),
|
|
24
21
|
tool_name: Schema.String,
|
|
25
|
-
tool_input: Schema.Record(
|
|
26
|
-
key: Schema.String,
|
|
27
|
-
value: Schema.Unknown
|
|
28
|
-
}),
|
|
22
|
+
tool_input: Schema.Record(Schema.String, Schema.Unknown),
|
|
29
23
|
tool_response: Schema.Struct({
|
|
30
24
|
interrupted: Schema.optional(Schema.Boolean),
|
|
31
25
|
exit_code: Schema.optional(Schema.Number),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Layer,
|
|
1
|
+
import { Layer, Logger, References } from "effect";
|
|
2
2
|
|
|
3
3
|
//#region src/commitlint/hook/silence-logger.ts
|
|
4
4
|
/**
|
|
@@ -7,13 +7,13 @@ import { Layer, LogLevel, Logger } from "effect";
|
|
|
7
7
|
* @remarks
|
|
8
8
|
* Hook subcommands reserve stdout for the JSON envelope they emit to Claude
|
|
9
9
|
* Code. Effect's default logger writes messages to stdout via `console.log`,
|
|
10
|
-
* which pollutes that contract (e.g.,
|
|
11
|
-
*
|
|
10
|
+
* which pollutes that contract (e.g., workspace discovery may log on every
|
|
11
|
+
* CLI invocation). This Layer combines two protections:
|
|
12
12
|
*
|
|
13
|
-
* 1.
|
|
13
|
+
* 1. `References.MinimumLogLevel` at `Warn` silences routine Info/Debug
|
|
14
14
|
* traffic so the common case never reaches the logger at all.
|
|
15
|
-
* 2. A
|
|
16
|
-
* stderr, so even
|
|
15
|
+
* 2. A logger-set replacement that routes anything which *does* fire to
|
|
16
|
+
* stderr, so even Warn/Error output cannot corrupt the stdout envelope.
|
|
17
17
|
*
|
|
18
18
|
* Only the hook command wrappers provide this Layer, so regular `savvy`
|
|
19
19
|
* commands keep their stdout output intact.
|
|
@@ -21,16 +21,18 @@ import { Layer, LogLevel, Logger } from "effect";
|
|
|
21
21
|
* @internal
|
|
22
22
|
*/
|
|
23
23
|
/**
|
|
24
|
-
* Replacement
|
|
25
|
-
*
|
|
24
|
+
* Replacement logger that writes every message to stderr, leaving stdout
|
|
25
|
+
* reserved for the hook's JSON envelope. `Logger.layer` without
|
|
26
|
+
* `mergeWithExisting` REPLACES the current logger set, so the default
|
|
27
|
+
* stdout logger never fires.
|
|
26
28
|
*/
|
|
27
|
-
const StderrLogger = Logger.
|
|
29
|
+
const StderrLogger = Logger.layer([Logger.make(({ message }) => {
|
|
28
30
|
process.stderr.write(`${JSON.stringify(message)}\n`);
|
|
29
|
-
}));
|
|
31
|
+
})]);
|
|
30
32
|
/** Minimum log level layer that suppresses Info and below. */
|
|
31
|
-
const MinLogLevel =
|
|
33
|
+
const MinLogLevel = Layer.succeed(References.MinimumLogLevel, "Warn");
|
|
32
34
|
/**
|
|
33
|
-
* Combined hook logger layer: stderr redirect merged with the
|
|
35
|
+
* Combined hook logger layer: stderr redirect merged with the Warn minimum
|
|
34
36
|
* log level. Provided by hook command wrappers to keep stdout corruption-proof.
|
|
35
37
|
*/
|
|
36
38
|
const HookSilencer = Layer.merge(StderrLogger, MinLogLevel);
|