@dzhechkov/harness-core 0.8.28 → 0.8.29
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/.dz-manifest.json +91 -31
- package/README.md +15 -2
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +10 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/cmd-usage.d.ts.map +1 -1
- package/dist/cmd-usage.js +22 -10
- package/dist/cmd-usage.js.map +1 -1
- package/dist/core-boundary.d.ts +1 -1
- package/dist/core-boundary.d.ts.map +1 -1
- package/dist/core-boundary.js +8 -1
- package/dist/core-boundary.js.map +1 -1
- package/dist/guard.d.ts +19 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +110 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/publish.d.ts +9 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +82 -0
- package/dist/publish.js.map +1 -1
- package/dist/release-line.d.ts +10 -0
- package/dist/release-line.d.ts.map +1 -0
- package/dist/release-line.js +21 -0
- package/dist/release-line.js.map +1 -0
- package/dist/repo-boundary.d.ts +13 -0
- package/dist/repo-boundary.d.ts.map +1 -0
- package/dist/repo-boundary.js +16 -0
- package/dist/repo-boundary.js.map +1 -0
- package/dist/repository-origin.d.ts +2 -0
- package/dist/repository-origin.d.ts.map +1 -0
- package/dist/repository-origin.js +2 -0
- package/dist/repository-origin.js.map +1 -0
- package/package.json +6 -6
- package/sbom.json +180 -30
- package/src/amendment-trace.ts +10 -1
- package/src/cmd-usage.ts +13 -6
- package/src/core-boundary.ts +7 -1
- package/src/guard.ts +137 -1
- package/src/index.ts +6 -1
- package/src/publish.ts +84 -0
- package/src/release-line.ts +31 -0
- package/src/repo-boundary.ts +25 -0
- package/src/repository-origin.ts +1 -0
package/src/guard.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type VolumeShadowInput,
|
|
22
22
|
type VolumeShadowResult,
|
|
23
23
|
} from './guard-volume.js';
|
|
24
|
+
import { findReleaseLine } from './release-line.js';
|
|
24
25
|
|
|
25
26
|
export type GuardSeverity = 'hard' | 'soft';
|
|
26
27
|
export type GuardOp = 'publish' | 'teach' | 'consolidate' | 'reindex';
|
|
@@ -79,6 +80,21 @@ export interface GuardResult {
|
|
|
79
80
|
/** Facts the CLI injects; each rule reads only the fields it needs. Missing evidence ⇒ that rule is skipped. */
|
|
80
81
|
export interface GuardFacts {
|
|
81
82
|
readonly op: GuardOp;
|
|
83
|
+
/** Signature evidence gathered by the CLI. The pure evaluator never reads manifests or keys. */
|
|
84
|
+
readonly signedPacks?: readonly {
|
|
85
|
+
readonly name: string;
|
|
86
|
+
readonly dir: string;
|
|
87
|
+
readonly changed: boolean;
|
|
88
|
+
readonly ok: boolean | null;
|
|
89
|
+
readonly failures: readonly string[];
|
|
90
|
+
readonly note?: string;
|
|
91
|
+
}[];
|
|
92
|
+
/** Release-line evidence gathered by the CLI; absence means the rule was not established. */
|
|
93
|
+
readonly releaseLines?: {
|
|
94
|
+
readonly readmes: readonly { readonly path: string; readonly text: string | null }[];
|
|
95
|
+
readonly coreVersion: string | null;
|
|
96
|
+
readonly cliVersion: string | null;
|
|
97
|
+
};
|
|
82
98
|
/** Publish-only raw volume facts. Absence preserves the legacy result shape. */
|
|
83
99
|
readonly volume?: VolumeShadowInput;
|
|
84
100
|
/** for no-workspace-star: each publishable package's deps map. */
|
|
@@ -165,7 +181,7 @@ export interface GuardFacts {
|
|
|
165
181
|
/** for skills-registrable: per skill pack, dirs that would ship un-registrable (no depth-1 SKILL.md). */
|
|
166
182
|
readonly skillPacks?: readonly { readonly name: string; readonly nonRegistrable: readonly string[] }[];
|
|
167
183
|
/** for readme-first: per publishable package, is a version bump staged without a README change? */
|
|
168
|
-
readonly readmeFirst?: readonly { readonly name: string; readonly versionBumped: boolean; readonly readmeChanged: boolean }[];
|
|
184
|
+
readonly readmeFirst?: readonly { readonly name: string; readonly versionBumped: boolean; readonly readmeChanged: boolean; readonly versionUnknown?: boolean }[];
|
|
169
185
|
/**
|
|
170
186
|
* for review-round: per publishable package, does this change bump a version AND touch SOURCE, and
|
|
171
187
|
* did it bring a GRADED QE report with it? `undefined` (the whole fact absent) means the tree could
|
|
@@ -347,6 +363,99 @@ function unquoteYaml(s: string): string {
|
|
|
347
363
|
return t;
|
|
348
364
|
}
|
|
349
365
|
|
|
366
|
+
function inspectReleaseLines(
|
|
367
|
+
evidence: NonNullable<GuardFacts['releaseLines']>,
|
|
368
|
+
severity: GuardSeverity,
|
|
369
|
+
): {
|
|
370
|
+
readonly violations: Violation[];
|
|
371
|
+
readonly observations: GuardObservation[];
|
|
372
|
+
} {
|
|
373
|
+
const expected = evidence.coreVersion !== null && evidence.cliVersion !== null
|
|
374
|
+
? { core: evidence.coreVersion, cli: evidence.cliVersion }
|
|
375
|
+
: null;
|
|
376
|
+
const violations: Violation[] = [];
|
|
377
|
+
const observations: GuardObservation[] = [];
|
|
378
|
+
|
|
379
|
+
for (const readme of evidence.readmes) {
|
|
380
|
+
if (readme.text === null) {
|
|
381
|
+
observations.push({
|
|
382
|
+
schemaVersion: 'volume-shadow/v1', rule: 'release-line-in-sync' as never,
|
|
383
|
+
metric: 'release-line-version-sync', scope: readme.path, status: 'unknown', value: null,
|
|
384
|
+
unit: 'artifact_set', signal: false, operands: {}, method: 'release-line-regex/v1',
|
|
385
|
+
detail: 'файл не прочитан',
|
|
386
|
+
});
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const found = findReleaseLine(readme.text);
|
|
390
|
+
if (found === null) {
|
|
391
|
+
observations.push({
|
|
392
|
+
schemaVersion: 'volume-shadow/v1', rule: 'release-line-in-sync' as never,
|
|
393
|
+
metric: 'release-line-version-sync', scope: readme.path, status: 'unknown', value: null,
|
|
394
|
+
unit: 'artifact_set', signal: false, operands: {}, method: 'release-line-regex/v1',
|
|
395
|
+
detail: 'строка релиза не найдена',
|
|
396
|
+
});
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (expected === null) {
|
|
400
|
+
const missing = [
|
|
401
|
+
...(evidence.coreVersion === null ? ['harness-core package.json version'] : []),
|
|
402
|
+
...(evidence.cliVersion === null ? ['harness-cli package.json version'] : []),
|
|
403
|
+
];
|
|
404
|
+
observations.push({
|
|
405
|
+
schemaVersion: 'volume-shadow/v1', rule: 'release-line-in-sync' as never,
|
|
406
|
+
metric: 'release-line-version-sync', scope: readme.path, status: 'unknown', value: [found.core, found.cli],
|
|
407
|
+
unit: 'artifact_set', signal: false, operands: { actual: [found.core, found.cli] },
|
|
408
|
+
method: 'release-line-regex/v1', detail: `${missing.join(' и ')} не прочитана или не имеет форму N.N.N`,
|
|
409
|
+
});
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const mismatch = found.core !== expected.core || found.cli !== expected.cli;
|
|
413
|
+
const detail = mismatch
|
|
414
|
+
? `${readme.path}: строка релиза говорит core v${found.core}/cli v${found.cli}, package.json — v${expected.core}/v${expected.cli}`
|
|
415
|
+
: `${readme.path}: строка релиза совпадает с package.json (${expected.core}/${expected.cli})`;
|
|
416
|
+
observations.push({
|
|
417
|
+
schemaVersion: 'volume-shadow/v1', rule: 'release-line-in-sync' as never,
|
|
418
|
+
metric: 'release-line-version-sync', scope: readme.path,
|
|
419
|
+
status: mismatch ? 'outside-reference' : 'within-reference', value: [found.core, found.cli],
|
|
420
|
+
unit: 'artifact_set', signal: mismatch,
|
|
421
|
+
operands: { actual: [found.core, found.cli], expected: [expected.core, expected.cli] },
|
|
422
|
+
method: 'release-line-regex/v1', detail,
|
|
423
|
+
});
|
|
424
|
+
if (mismatch) violations.push({ rule: 'release-line-in-sync', severity, detail });
|
|
425
|
+
}
|
|
426
|
+
return { violations, observations };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function inspectSignatureFresh(
|
|
430
|
+
evidence: NonNullable<GuardFacts['signedPacks']>,
|
|
431
|
+
severity: GuardSeverity,
|
|
432
|
+
): {
|
|
433
|
+
readonly violations: Violation[];
|
|
434
|
+
readonly observations: GuardObservation[];
|
|
435
|
+
} {
|
|
436
|
+
const violations: Violation[] = [];
|
|
437
|
+
const observations: GuardObservation[] = [];
|
|
438
|
+
for (const pack of evidence) {
|
|
439
|
+
if (!pack.changed) continue;
|
|
440
|
+
if (pack.ok === false) {
|
|
441
|
+
const firstFailure = pack.failures[0] ?? 'verification failed without a named reason';
|
|
442
|
+
violations.push({
|
|
443
|
+
rule: 'signature-fresh',
|
|
444
|
+
severity,
|
|
445
|
+
detail: `${pack.name}: files changed but the signed manifest is stale (${firstFailure}) — re-sign: dz sign --pack ${pack.dir} --key <key>`,
|
|
446
|
+
});
|
|
447
|
+
} else if (pack.ok === true) {
|
|
448
|
+
observations.push({
|
|
449
|
+
schemaVersion: 'volume-shadow/v1', rule: 'signature-fresh' as never,
|
|
450
|
+
metric: 'signed-manifest-freshness', scope: pack.name, status: 'within-reference', value: 1,
|
|
451
|
+
unit: 'artifact_set', signal: false, operands: { changed: 1, verified: 1 },
|
|
452
|
+
method: 'verify-manifest/v1', detail: `${pack.name}: ${pack.note ?? 'changed files still match its signed manifest'}`,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return { violations, observations };
|
|
457
|
+
}
|
|
458
|
+
|
|
350
459
|
/** The built-in rule set (works with no config). Ops are the mutating operations each rule guards. */
|
|
351
460
|
export const DEFAULT_RULES: readonly GuardRule[] = [
|
|
352
461
|
{ id: 'no-workspace-star', severity: 'hard', ops: ['publish'], description: 'a published package.json must carry no workspace:* dep (npm ships it verbatim → the install breaks)' },
|
|
@@ -356,6 +465,8 @@ export const DEFAULT_RULES: readonly GuardRule[] = [
|
|
|
356
465
|
{ id: 'backlog-covers-features', severity: 'soft', ops: ['publish', 'consolidate'], description: 'каталог фичи, заведённый после базовой даты, назван записью бэклога — либо несёт именованную оговорку с причиной' },
|
|
357
466
|
{ id: 'no-secrets', severity: 'hard', ops: ['teach', 'publish'], description: 'no private key or API token in lesson text or a published file' },
|
|
358
467
|
{ id: 'readme-consistency', severity: 'soft', ops: ['publish'], description: 'README counts agree (CJM header vs All Commands, etc.)' },
|
|
468
|
+
{ id: 'release-line-in-sync', severity: 'soft', ops: ['publish'], description: 'root and harness-cli README release lines agree with the harness-core and harness-cli package versions' },
|
|
469
|
+
{ id: 'signature-fresh', severity: 'soft', ops: ['publish'], description: 'a pack whose files changed in this diff still verifies against its signed .dz-manifest.json — a stale signature is named before publish, not at the gate' },
|
|
359
470
|
{ id: 'skills-registrable', severity: 'soft', ops: ['publish'], description: 'every skill directory in a skill pack has a depth-1 SKILL.md (a buried or missing one ships un-registrable — the health-advisor 1.2.0 class)' },
|
|
360
471
|
{ id: 'readme-first', severity: 'soft', ops: ['publish'], description: 'a package with a staged version bump must update its own README.md in the same change (README-first)' },
|
|
361
472
|
{ id: 'routing-store-stale', severity: 'soft', ops: ['publish'], description: 'harvested routing telemetry has been applied to the auto-cost outcome store' },
|
|
@@ -921,6 +1032,8 @@ const HAS_INPUT: Partial<Record<string, (f: GuardFacts) => boolean>> = {
|
|
|
921
1032
|
// Дерево без плагин-манифестов правилу нечего сказать: «прошло» тут значило бы «не смотрели».
|
|
922
1033
|
'plugin-manifest-audit': (f) => Array.isArray(f.pluginManifests) && f.pluginManifests.length > 0,
|
|
923
1034
|
'no-secrets': (f) => Array.isArray(f.secretTargets) && f.secretTargets.length > 0,
|
|
1035
|
+
'release-line-in-sync': (f) => typeof f.releaseLines === 'object' && f.releaseLines !== null,
|
|
1036
|
+
'signature-fresh': (f) => Array.isArray(f.signedPacks),
|
|
924
1037
|
};
|
|
925
1038
|
|
|
926
1039
|
/**
|
|
@@ -1065,6 +1178,29 @@ export function evaluateGuard(facts: GuardFacts, rules: readonly GuardRule[] = D
|
|
|
1065
1178
|
continue;
|
|
1066
1179
|
}
|
|
1067
1180
|
checked.push(r.id);
|
|
1181
|
+
if (r.id === 'readme-first') {
|
|
1182
|
+
for (const p of facts.readmeFirst ?? []) {
|
|
1183
|
+
if (p?.versionUnknown !== true) continue;
|
|
1184
|
+
observations.push({
|
|
1185
|
+
schemaVersion: 'volume-shadow/v1', rule: 'readme-first' as never,
|
|
1186
|
+
metric: 'package-version-changed-from-head', scope: p.name, status: 'unknown', value: null,
|
|
1187
|
+
unit: 'artifact_set', signal: false, operands: {}, method: 'git-show-head-package-version/v1',
|
|
1188
|
+
detail: `${p.name}: package version could not be compared with HEAD; readme-first stayed advisory and emitted no violation`,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (r.id === 'release-line-in-sync') {
|
|
1193
|
+
const inspection = inspectReleaseLines(facts.releaseLines!, r.severity);
|
|
1194
|
+
observations.push(...inspection.observations);
|
|
1195
|
+
violations.push(...inspection.violations);
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (r.id === 'signature-fresh') {
|
|
1199
|
+
const inspection = inspectSignatureFresh(facts.signedPacks!, r.severity);
|
|
1200
|
+
observations.push(...inspection.observations);
|
|
1201
|
+
violations.push(...inspection.violations);
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1068
1204
|
if ((VOLUME_SHADOW_RULE_IDS as readonly string[]).includes(r.id)) {
|
|
1069
1205
|
const emission = volume();
|
|
1070
1206
|
observations.push(...emission.observations.filter((item) => item.rule === r.id));
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { createRequire } from 'node:module';
|
|
|
10
10
|
export const HARNESS_CORE_VERSION: string =
|
|
11
11
|
(createRequire(import.meta.url)('../package.json') as { version: string }).version;
|
|
12
12
|
|
|
13
|
+
export { REPOSITORY_ORIGIN } from './repository-origin.js';
|
|
14
|
+
|
|
13
15
|
export * from './skills.js';
|
|
14
16
|
export * from './apply.js';
|
|
15
17
|
export {
|
|
@@ -123,6 +125,8 @@ export { planLedgerBackfill, LEDGER_FILL_SOURCE, AMBIGUOUS, resolveLedgerRunId }
|
|
|
123
125
|
// project-skills root resolution (field report doc-25b): the ONE builder behind both the Step-0
|
|
124
126
|
// probe and the PS_GUIDANCE paragraph, so the two can never look at different roots again.
|
|
125
127
|
export { projectSkillsOneRoot, projectSkillsProbeCommand } from './project-skills-root.js';
|
|
128
|
+
export { isRepoBoundary } from './repo-boundary.js';
|
|
129
|
+
export type { RepoBoundaryIo } from './repo-boundary.js';
|
|
126
130
|
export type { LedgerBackfillPlan, LedgerBackfillRow, RunCostFacts } from './ledger-backfill.js';
|
|
127
131
|
export type { SweepResult, DriftedSkill, SyncResult, SyncCanonicalOptions } from './skill-drift.js';
|
|
128
132
|
export { benchmarkSkill, benchmarkSkills, compareSkills } from './benchmark.js';
|
|
@@ -660,7 +664,8 @@ export type {
|
|
|
660
664
|
ChainDefectAges,
|
|
661
665
|
ChainedJournal,
|
|
662
666
|
} from './event-chain.js';
|
|
663
|
-
export { decideProvenance, environmentCanMintProvenance, publishArgv, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion } from './publish.js';
|
|
667
|
+
export { decideProvenance, environmentCanMintProvenance, publishArgv, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, rewriteWorkspaceSpecs, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion } from './publish.js';
|
|
668
|
+
export { RELEASE_LINE_RE, findReleaseLine, rewriteReleaseLine } from './release-line.js';
|
|
664
669
|
export * from './course-staleness.js';
|
|
665
670
|
export { fetchAllDownloads } from './downloads.js';
|
|
666
671
|
export type { PackageDownloads, DownloadsReport } from './downloads.js';
|
package/src/publish.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { execSync } from 'node:child_process';
|
|
|
16
16
|
type ExecSyncOptionsWithStringEncoding = NonNullable<Parameters<typeof execSync>[1]> & { encoding: 'utf-8' };
|
|
17
17
|
|
|
18
18
|
import { claimCheck } from './claim-check.js';
|
|
19
|
+
import { rewriteReleaseLine } from './release-line.js';
|
|
19
20
|
|
|
20
21
|
export type ProbeOutcome = {
|
|
21
22
|
readonly attempt: number;
|
|
@@ -67,6 +68,10 @@ export interface PublishReport {
|
|
|
67
68
|
readonly skipped: number;
|
|
68
69
|
readonly errors: number;
|
|
69
70
|
readonly dryRun: boolean;
|
|
71
|
+
/** Repo-relative README paths whose first joint core/CLI release line was rewritten. */
|
|
72
|
+
readonly releaseLineSynced: readonly string[];
|
|
73
|
+
/** Post-publication sync failures are warnings: registry-confirmed packages cannot be unpublished. */
|
|
74
|
+
readonly warnings?: readonly string[] | undefined;
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
/** Is `p` inside `dir`? Used to refuse a signing key that lives in the repository working tree. */
|
|
@@ -130,6 +135,32 @@ function maxPublished(name: string, localVersion: string, exec: PublishExec = ex
|
|
|
130
135
|
// the publish itself succeeds, and every consumer `npm install` then fails with ETARGET. Staged is
|
|
131
136
|
// not shipped; this preflight makes the difference a refusal instead of a broken release.
|
|
132
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Mirror pnpm's package-time expansion of the three shorthand workspace dependency specs.
|
|
140
|
+
* Pure by construction: callers provide both the source bytes and the sibling version table.
|
|
141
|
+
*/
|
|
142
|
+
export function rewriteWorkspaceSpecs(
|
|
143
|
+
pkgJsonText: string,
|
|
144
|
+
siblingVersions: ReadonlyMap<string, string>,
|
|
145
|
+
): string {
|
|
146
|
+
const pkg = JSON.parse(pkgJsonText) as Record<string, unknown>;
|
|
147
|
+
const fields = ['dependencies', 'peerDependencies', 'optionalDependencies', 'devDependencies'] as const;
|
|
148
|
+
for (const field of fields) {
|
|
149
|
+
const candidate = pkg[field];
|
|
150
|
+
if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
|
|
151
|
+
const table = candidate as Record<string, unknown>;
|
|
152
|
+
for (const [dep, spec] of Object.entries(table)) {
|
|
153
|
+
if (typeof spec !== 'string') continue;
|
|
154
|
+
const match = /^workspace:([*^~])$/.exec(spec);
|
|
155
|
+
const version = siblingVersions.get(dep);
|
|
156
|
+
if (match === null || version === undefined) continue;
|
|
157
|
+
const marker = match[1]!;
|
|
158
|
+
table[dep] = marker === '*' ? version : `${marker}${version}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return JSON.stringify(pkg, null, 2) + '\n';
|
|
162
|
+
}
|
|
163
|
+
|
|
133
164
|
/**
|
|
134
165
|
* Pure half: which `workspace:`-declared deps of a package would pack to a floor that is neither
|
|
135
166
|
* being published in this batch nor already on the registry?
|
|
@@ -892,11 +923,64 @@ export function publishPackages(
|
|
|
892
923
|
}
|
|
893
924
|
}
|
|
894
925
|
|
|
926
|
+
const releaseLineSynced: string[] = [];
|
|
927
|
+
const warnings: string[] = [];
|
|
928
|
+
const releasePackageNames = new Set(['@dzhechkov/harness-core', '@dzhechkov/harness-cli']);
|
|
929
|
+
const releasePackagePublished = results.some(
|
|
930
|
+
(result) => result.status === 'published' && releasePackageNames.has(result.name),
|
|
931
|
+
);
|
|
932
|
+
if (releasePackagePublished && opts.dryRun !== true && opts.bumpOnly !== true) {
|
|
933
|
+
const currentVersion = (name: string): string | null => {
|
|
934
|
+
const landed = results.find((result) => result.name === name && result.status === 'published');
|
|
935
|
+
if (landed !== undefined) return landed.newVersion;
|
|
936
|
+
const pkg = packages.find((candidate) => candidate.name === name);
|
|
937
|
+
if (pkg === undefined) return null;
|
|
938
|
+
try {
|
|
939
|
+
const parsed = JSON.parse(readFileSync(pathJoin(pkg.dir, 'package.json'), 'utf8')) as { version?: unknown };
|
|
940
|
+
return typeof parsed.version === 'string' ? parsed.version : null;
|
|
941
|
+
} catch {
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
const coreVersion = currentVersion('@dzhechkov/harness-core');
|
|
946
|
+
const cliVersion = currentVersion('@dzhechkov/harness-cli');
|
|
947
|
+
if (coreVersion === null || cliVersion === null) {
|
|
948
|
+
warnings.push('release-line sync skipped: could not read both harness-core and harness-cli package versions');
|
|
949
|
+
} else {
|
|
950
|
+
const readmes = [
|
|
951
|
+
{ path: 'README.md', absolute: pathJoin(monorepoRoot, 'README.md') },
|
|
952
|
+
{
|
|
953
|
+
path: 'packages/@dzhechkov/harness-cli/README.md',
|
|
954
|
+
absolute: pathJoin(monorepoRoot, 'packages', '@dzhechkov', 'harness-cli', 'README.md'),
|
|
955
|
+
},
|
|
956
|
+
] as const;
|
|
957
|
+
for (const readme of readmes) {
|
|
958
|
+
try {
|
|
959
|
+
const original = readFileSync(readme.absolute, 'utf8');
|
|
960
|
+
const updated = rewriteReleaseLine(original, coreVersion, cliVersion);
|
|
961
|
+
if (updated === null) {
|
|
962
|
+
warnings.push(`release-line sync skipped ${readme.path}: release line not found`);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (updated === original) continue;
|
|
966
|
+
const tmp = readme.absolute + '.sync-tmp';
|
|
967
|
+
writeFileSync(tmp, updated);
|
|
968
|
+
renameSync(tmp, readme.absolute);
|
|
969
|
+
releaseLineSynced.push(readme.path);
|
|
970
|
+
} catch (error) {
|
|
971
|
+
warnings.push(`release-line sync failed ${readme.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
895
977
|
return {
|
|
896
978
|
packages: results,
|
|
897
979
|
published: results.filter((r) => r.status === 'published').length,
|
|
898
980
|
skipped: results.filter((r) => r.status === 'skipped').length,
|
|
899
981
|
errors: results.filter((r) => r.status === 'error').length,
|
|
900
982
|
dryRun: opts.dryRun === true,
|
|
983
|
+
releaseLineSynced,
|
|
984
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
901
985
|
};
|
|
902
986
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const RELEASE_LINE_RE = /`harness-core v(\d+\.\d+\.\d+)` · `harness-cli v(\d+\.\d+\.\d+)`/;
|
|
2
|
+
|
|
3
|
+
export interface ReleaseLineMatch {
|
|
4
|
+
readonly index: number;
|
|
5
|
+
readonly line: string;
|
|
6
|
+
readonly core: string;
|
|
7
|
+
readonly cli: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function findReleaseLine(text: string): ReleaseLineMatch | null {
|
|
11
|
+
const lines = text.split('\n');
|
|
12
|
+
for (let index = 0; index < lines.length; index++) {
|
|
13
|
+
const line = lines[index]!;
|
|
14
|
+
const match = RELEASE_LINE_RE.exec(line);
|
|
15
|
+
if (match?.[1] !== undefined && match[2] !== undefined) {
|
|
16
|
+
return { index, line, core: match[1], cli: match[2] };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function rewriteReleaseLine(text: string, core: string, cli: string): string | null {
|
|
23
|
+
const found = findReleaseLine(text);
|
|
24
|
+
if (found === null) return null;
|
|
25
|
+
const lines = text.split('\n');
|
|
26
|
+
lines[found.index] = found.line.replace(
|
|
27
|
+
RELEASE_LINE_RE,
|
|
28
|
+
`\`harness-core v${core}\` · \`harness-cli v${cli}\``,
|
|
29
|
+
);
|
|
30
|
+
return lines.join('\n');
|
|
31
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Filesystem facts required to recognise a real Git repository boundary. */
|
|
2
|
+
export interface RepoBoundaryIo {
|
|
3
|
+
exists(p: string): boolean;
|
|
4
|
+
isDirectory(p: string): boolean;
|
|
5
|
+
readText(p: string): string | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A Git repository boundary is either a `.git` directory containing `HEAD`, or a worktree-style
|
|
10
|
+
* `.git` file whose first bytes are the `gitdir:` redirect. An empty or unrelated `.git` entry is
|
|
11
|
+
* not a boundary.
|
|
12
|
+
*/
|
|
13
|
+
export function isRepoBoundary(
|
|
14
|
+
dir: string,
|
|
15
|
+
io: RepoBoundaryIo,
|
|
16
|
+
join: (...p: string[]) => string,
|
|
17
|
+
): boolean {
|
|
18
|
+
const git = join(dir, '.git');
|
|
19
|
+
if (!io.exists(git)) return false;
|
|
20
|
+
if (io.isDirectory(git)) {
|
|
21
|
+
const head = join(git, 'HEAD');
|
|
22
|
+
return io.exists(head) && !io.isDirectory(head);
|
|
23
|
+
}
|
|
24
|
+
return io.readText(git)?.startsWith('gitdir:') === true;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const REPOSITORY_ORIGIN = 'git+https://github.com/djd1m/dz-harness-hub.git';
|