release-skill 0.1.4 → 0.1.6
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/CHANGELOG.md +104 -0
- package/INSTALL.md +81 -1
- package/INSTALL.zh-CN.md +69 -1
- package/README.md +233 -8
- package/README.zh-CN.md +188 -8
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +14164 -9912
- package/adapters/claude/bin/release-skill.mjs +24 -4
- package/adapters/claude/native/safe-write/binding.gyp +2 -1
- package/adapters/claude/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/adapters/claude/native/safe-write/prebuilds.json +1 -1
- package/adapters/claude/schemas/.render-manifest.json +10 -10
- package/adapters/claude/schemas/release-project.schema.json +141 -0
- package/adapters/claude/skills/release-help/SKILL.md +21 -0
- package/adapters/claude/skills/release-prepare/SKILL.md +17 -6
- package/adapters/claude/skills/release-publish/SKILL.md +3 -1
- package/adapters/claude/skills/release-reconcile/SKILL.md +1 -1
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +14164 -9912
- package/adapters/codex/bin/release-skill.mjs +24 -4
- package/adapters/codex/native/safe-write/binding.gyp +2 -1
- package/adapters/codex/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/adapters/codex/native/safe-write/prebuilds.json +1 -1
- package/adapters/codex/schemas/.render-manifest.json +10 -10
- package/adapters/codex/schemas/release-project.schema.json +141 -0
- package/adapters/codex/skills/release-help/SKILL.md +21 -0
- package/adapters/codex/skills/release-prepare/SKILL.md +17 -6
- package/adapters/codex/skills/release-publish/SKILL.md +3 -1
- package/adapters/codex/skills/release-reconcile/SKILL.md +1 -1
- package/bin/release-skill-cli.mjs +163 -4
- package/bin/release-skill.bundle.mjs +14164 -9912
- package/bin/release-skill.mjs +24 -4
- package/native/safe-write/binding.gyp +2 -1
- package/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/native/safe-write/prebuilds.json +1 -1
- package/package.json +2 -2
- package/references/.render-manifest.json +4 -4
- package/references/02-project-config.md +24 -0
- package/references/05-evidence-and-errors.md +5 -0
- package/schemas/.render-manifest.json +10 -10
- package/schemas/release-project.schema.json +141 -0
- package/scripts/build-bundle.mjs +15 -2
- package/skills/release-help/SKILL.md +21 -0
- package/skills/release-prepare/SKILL.md +17 -6
- package/skills/release-publish/SKILL.md +3 -1
- package/skills/release-reconcile/SKILL.md +1 -1
- package/skills-src/release-help/SKILL.md +21 -0
- package/skills-src/release-prepare/SKILL.md +17 -6
- package/skills-src/release-publish/SKILL.md +3 -1
- package/skills-src/release-reconcile/SKILL.md +1 -1
- package/src/adapters/plugin-marketplace.mjs +70 -3
- package/src/artifacts/transaction-journal.mjs +1126 -105
- package/src/artifacts/transaction.mjs +313 -130
- package/src/commands/docs.mjs +332 -0
- package/src/commands/prepare.mjs +324 -17
- package/src/commands/reconcile.mjs +4 -1
- package/src/commands/verify.mjs +4 -1
- package/src/core/errors.mjs +64 -2
- package/src/core/plan.mjs +59 -1
- package/src/core/redact.mjs +206 -0
- package/src/docs/changelog-renderer.mjs +853 -0
- package/src/docs/config.mjs +337 -0
- package/src/docs/notes-loader.mjs +432 -0
- package/src/docs/notes.mjs +553 -0
- package/src/docs/readme-renderer.mjs +647 -0
- package/src/docs/refresh-planner.mjs +542 -0
- package/src/docs/refresh-service.mjs +675 -0
package/src/core/plan.mjs
CHANGED
|
@@ -312,9 +312,13 @@ const REQUIRED_ACTION_TYPES = ['push-snapshot', 'create-tag', 'github-release'];
|
|
|
312
312
|
* (e.g. v0.0.10 must not match expected version 0.0.1).
|
|
313
313
|
*
|
|
314
314
|
* @param {object} plan - A validated release plan object.
|
|
315
|
+
* @param {object} [options]
|
|
316
|
+
* @param {boolean} [options.legacyCompatibility] - When true, relax
|
|
317
|
+
* timeoutMs requirement for old plans (reconcile/verify paths).
|
|
318
|
+
* New prepare/approve/publish must not set this flag.
|
|
315
319
|
* @returns {{ passed: boolean, details: { failures: string[], expectedCount: number, actualCount: number } }}
|
|
316
320
|
*/
|
|
317
|
-
export function validatePlanActionCompleteness(plan) {
|
|
321
|
+
export function validatePlanActionCompleteness(plan, options = {}) {
|
|
318
322
|
const failures = [];
|
|
319
323
|
|
|
320
324
|
if (!plan || typeof plan !== 'object') {
|
|
@@ -670,6 +674,33 @@ export function validatePlanActionCompleteness(plan) {
|
|
|
670
674
|
_checkRequired(action, 'parameters.ref', action.parameters?.ref, expectedTag, unitId, failures);
|
|
671
675
|
_checkRequired(action, 'parameters.manifestDigest', action.parameters?.manifestDigest, frozen?.manifestDigest, unitId, failures);
|
|
672
676
|
}
|
|
677
|
+
// timeoutMs is mandatory for all marketplace install actions.
|
|
678
|
+
// Legacy plans (pre-v0.1.5) lack this field; legacyCompatibility
|
|
679
|
+
// relaxes the check for reconcile/verify paths only, and only when
|
|
680
|
+
// the property is genuinely absent (undefined). An explicit null is
|
|
681
|
+
// NOT "absent" -- it is an invalid value and always fails closed,
|
|
682
|
+
// like strings or out-of-range numbers, even in legacyCompatibility.
|
|
683
|
+
{
|
|
684
|
+
const raw = action.parameters?.timeoutMs;
|
|
685
|
+
if (raw === undefined) {
|
|
686
|
+
if (!options.legacyCompatibility) {
|
|
687
|
+
failures.push(
|
|
688
|
+
`unit "${unitId}", action "${action.id}": parameters.timeoutMs is missing, expected a valid timeout (30000-900000)`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
// Field is present -- always validate range/type, even in legacy mode
|
|
693
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw) || !Number.isInteger(raw)) {
|
|
694
|
+
failures.push(
|
|
695
|
+
`unit "${unitId}", action "${action.id}": parameters.timeoutMs must be a finite integer, got: ${JSON.stringify(raw)}`,
|
|
696
|
+
);
|
|
697
|
+
} else if (raw < 30000 || raw > 900000) {
|
|
698
|
+
failures.push(
|
|
699
|
+
`unit "${unitId}", action "${action.id}": parameters.timeoutMs must be between 30000 and 900000, got: ${raw}`,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
673
704
|
|
|
674
705
|
// Expected checks
|
|
675
706
|
_checkRequired(action, 'expected.installed', action.expected?.installed, true, unitId, failures);
|
|
@@ -746,6 +777,33 @@ export function validatePlanActionCompleteness(plan) {
|
|
|
746
777
|
_checkRequired(action, 'parameters.ref', action.parameters?.ref, expectedTag, unitId, failures);
|
|
747
778
|
_checkRequired(action, 'parameters.manifestDigest', action.parameters?.manifestDigest, frozen?.manifestDigest, unitId, failures);
|
|
748
779
|
}
|
|
780
|
+
// timeoutMs is mandatory for all marketplace install actions.
|
|
781
|
+
// Legacy plans (pre-v0.1.5) lack this field; legacyCompatibility
|
|
782
|
+
// relaxes the check for reconcile/verify paths only, and only when
|
|
783
|
+
// the property is genuinely absent (undefined). An explicit null is
|
|
784
|
+
// NOT "absent" -- it is an invalid value and always fails closed,
|
|
785
|
+
// like strings or out-of-range numbers, even in legacyCompatibility.
|
|
786
|
+
{
|
|
787
|
+
const raw = action.parameters?.timeoutMs;
|
|
788
|
+
if (raw === undefined) {
|
|
789
|
+
if (!options.legacyCompatibility) {
|
|
790
|
+
failures.push(
|
|
791
|
+
`unit "${unitId}", action "${action.id}": parameters.timeoutMs is missing, expected a valid timeout (30000-900000)`,
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
} else {
|
|
795
|
+
// Field is present -- always validate range/type, even in legacy mode
|
|
796
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw) || !Number.isInteger(raw)) {
|
|
797
|
+
failures.push(
|
|
798
|
+
`unit "${unitId}", action "${action.id}": parameters.timeoutMs must be a finite integer, got: ${JSON.stringify(raw)}`,
|
|
799
|
+
);
|
|
800
|
+
} else if (raw < 30000 || raw > 900000) {
|
|
801
|
+
failures.push(
|
|
802
|
+
`unit "${unitId}", action "${action.id}": parameters.timeoutMs must be between 30000 and 900000, got: ${raw}`,
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
749
807
|
|
|
750
808
|
// Expected checks
|
|
751
809
|
_checkRequired(action, 'expected.installed', action.expected?.installed, true, unitId, failures);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized redaction of sensitive filesystem paths in error outputs.
|
|
3
|
+
*
|
|
4
|
+
* Defect #3 fix: runtime error outputs (CLI text/JSON output and details
|
|
5
|
+
* structures) must never carry absolute filesystem paths (the macOS Users
|
|
6
|
+
* realm, the Linux home realm, the macOS private/var alias realm, temp
|
|
7
|
+
* roots, tmpdir fixture roots). This module is the single redaction
|
|
8
|
+
* authority consumed by the ReleaseError constructor choke point
|
|
9
|
+
* (core/errors.mjs); producers may also apply it defense-in-depth.
|
|
10
|
+
*
|
|
11
|
+
* Semantics:
|
|
12
|
+
* - Strings: tokens shaped like real absolute paths are replaced with the
|
|
13
|
+
* stable placeholder `<redacted-path>`. Three families are redacted
|
|
14
|
+
* fail-closed:
|
|
15
|
+
* (1) POSIX absolute paths (leading '/', at least two path segments) —
|
|
16
|
+
* unless the token classifies as a strict RFC 6901 JSON Pointer;
|
|
17
|
+
* (2) Windows drive-letter paths (X:\... and X:/...);
|
|
18
|
+
* (3) UNC paths (\\server\share...).
|
|
19
|
+
* Strict JSON Pointers (e.g. /frozenSnapshot/commitTimestamp,
|
|
20
|
+
* /units/0/version) are stable diagnostic coordinates, not filesystem
|
|
21
|
+
* paths, and are preserved verbatim. Relative fragments (no leading '/'),
|
|
22
|
+
* flag tokens (--unit), reason vocabulary (MISSING_VALUE), error codes,
|
|
23
|
+
* sha256 digests, stable field names, and fragment-anchored JSON pointers
|
|
24
|
+
* (#/required) are likewise preserved verbatim. Any other two-or-more
|
|
25
|
+
* segment '/'-led token stays redacted as the fail-closed default.
|
|
26
|
+
* - Arrays and plain objects: recursed; every string value is redacted.
|
|
27
|
+
* - Everything else (numbers, booleans, null, undefined, and non-plain
|
|
28
|
+
* objects such as Buffers/Dates/Maps) is returned untouched.
|
|
29
|
+
*
|
|
30
|
+
* Pure and zero-dependency: no imports from src/artifacts/* (no cycles, no
|
|
31
|
+
* cross-layer coupling); node built-ins only (none required).
|
|
32
|
+
*
|
|
33
|
+
* @module core/redact
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** Stable placeholder substituted for every redacted absolute path. */
|
|
37
|
+
export const REDACTED_PATH_PLACEHOLDER = '<redacted-path>';
|
|
38
|
+
|
|
39
|
+
// Absolute-path tokens. Three families, tried in order at each anchor point:
|
|
40
|
+
// (a) Windows drive-letter paths (C:\... or C:/...) — ordered before the
|
|
41
|
+
// POSIX alternative so a drive-letter token reaching into the Users
|
|
42
|
+
// realm collapses to a single placeholder instead of leaving a 'C:'
|
|
43
|
+
// prefix behind;
|
|
44
|
+
// (b) UNC paths (\\server\share...);
|
|
45
|
+
// (c) POSIX absolute paths: a '/' anchored at the start of the string or
|
|
46
|
+
// preceded by a delimiter (whitespace, quote, '=', ':', ',', '(', '[',
|
|
47
|
+
// '{', '<'), running until whitespace or a quote. The left boundary keeps
|
|
48
|
+
// redaction from firing inside relative fragments ('src/core/x.mjs'),
|
|
49
|
+
// fragment-anchored JSON pointers ('#/required'), or protocol-relative
|
|
50
|
+
// URLs ('https://x/y').
|
|
51
|
+
const PATH_TOKEN_RE =
|
|
52
|
+
/(?<=^|[\s'"=:,([{<])(?:[A-Za-z]:[\\/][^\s'"]*|\\\\[^\s'"]+|\/[^\s'"]+)/g;
|
|
53
|
+
|
|
54
|
+
// Trailing prose punctuation that may cling to a token and must survive.
|
|
55
|
+
const TRAILING_PUNCT_RE = /^(.*?)([.!,;:)\]}>]*)$/;
|
|
56
|
+
|
|
57
|
+
// Filesystem root directories that mark a '/'-led token as a real absolute
|
|
58
|
+
// path even when every segment is identifier-shaped (e.g. a temp-root or
|
|
59
|
+
// Users-realm path whose segments are all plain identifiers): the Unix FHS
|
|
60
|
+
// hierarchy, macOS realms, and common ephemeral/CI checkout roots.
|
|
61
|
+
// Fail-closed backstop under the JSON Pointer classifier.
|
|
62
|
+
const KNOWN_FS_ROOTS = new Set([
|
|
63
|
+
// Unix/Linux FHS
|
|
64
|
+
'bin', 'boot', 'dev', 'etc', 'home', 'lib', 'lib64', 'media', 'mnt',
|
|
65
|
+
'opt', 'proc', 'root', 'run', 'sbin', 'srv', 'sys', 'tmp', 'usr', 'var',
|
|
66
|
+
// macOS realms
|
|
67
|
+
'Applications', 'Library', 'Network', 'System', 'Users', 'Volumes',
|
|
68
|
+
'cores', 'private',
|
|
69
|
+
// common ephemeral / CI checkout roots
|
|
70
|
+
'app', 'build', 'data', 'dist', 'workspace', 'workspaces',
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
// Strict RFC 6901 reference-token shapes as used by diagnostic JSON Pointers
|
|
74
|
+
// in this system: identifier-like property names and array indexes (no
|
|
75
|
+
// leading zeros, per RFC 6901 array indexing).
|
|
76
|
+
const POINTER_IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
77
|
+
const POINTER_INDEX_RE = /^(?:0|[1-9][0-9]*)$/;
|
|
78
|
+
|
|
79
|
+
// JSON Pointer syntax and POSIX absolute-path syntax overlap. Preserve only
|
|
80
|
+
// the diagnostic pointer roots emitted by the plan/unit validators that need
|
|
81
|
+
// stable public diagnostics. A broad camelCase/array-index heuristic would
|
|
82
|
+
// incorrectly disclose real paths such as a custom mount with identifier-like
|
|
83
|
+
// segments, violating the fail-closed contract.
|
|
84
|
+
const DIAGNOSTIC_POINTER_ROOTS = new Set([
|
|
85
|
+
'frozenSnapshot',
|
|
86
|
+
'units',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Classify a '/'-led, >= 2-segment token as a strict RFC 6901 JSON Pointer
|
|
91
|
+
* (a stable diagnostic coordinate such as /units/0/frozenSnapshot/commitTimestamp
|
|
92
|
+
* or /frozenSnapshot/commitTimestamp) rather than a filesystem path.
|
|
93
|
+
*
|
|
94
|
+
* Conservative by design (fail-closed): the token qualifies only when its
|
|
95
|
+
* first segment belongs to an explicit diagnostic namespace and every segment
|
|
96
|
+
* is an identifier-like reference token or array index. Any other
|
|
97
|
+
* two-or-more-segment token is treated as a path and redacted.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} token
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
function isStrictJsonPointer(token) {
|
|
103
|
+
const segments = token.slice(1).split('/');
|
|
104
|
+
if (segments.length < 2) return false;
|
|
105
|
+
if (KNOWN_FS_ROOTS.has(segments[0])) return false;
|
|
106
|
+
if (!DIAGNOSTIC_POINTER_ROOTS.has(segments[0])) return false;
|
|
107
|
+
for (const segment of segments) {
|
|
108
|
+
if (!POINTER_INDEX_RE.test(segment) && !POINTER_IDENTIFIER_RE.test(segment)) {
|
|
109
|
+
// Dots (file extensions), hyphens, tildes, escapes, empty segments:
|
|
110
|
+
// not a strict diagnostic pointer — fall back to path redaction.
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Whether a token looks like a POSIX absolute path with >= 2 segments
|
|
119
|
+
* (e.g. a Users-realm path, a private/var-folders path, or a temp-root
|
|
120
|
+
* path) that is NOT a strict JSON Pointer. Single-segment tokens such as
|
|
121
|
+
* '/tmp', schema instance fragments such as '/:', and strict JSON Pointers
|
|
122
|
+
* such as '/frozenSnapshot/commitTimestamp' are left alone.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} token
|
|
125
|
+
* @returns {boolean}
|
|
126
|
+
*/
|
|
127
|
+
function looksLikeAbsolutePath(token) {
|
|
128
|
+
if (typeof token !== 'string' || token.length < 3) return false;
|
|
129
|
+
if (!token.startsWith('/') || token.startsWith('//')) return false;
|
|
130
|
+
if (!token.slice(1).includes('/')) return false;
|
|
131
|
+
return !isStrictJsonPointer(token);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whether a token is a Windows drive-letter absolute path (X:\... or X:/...).
|
|
136
|
+
* Drive-letter tokens are unambiguously filesystem paths and always redact.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} token
|
|
139
|
+
* @returns {boolean}
|
|
140
|
+
*/
|
|
141
|
+
function isWindowsDrivePath(token) {
|
|
142
|
+
return /^[A-Za-z]:[\\/]/.test(token);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Whether a token is a UNC path (\\server\share...). UNC tokens are
|
|
147
|
+
* unambiguously filesystem paths and always redact.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} token
|
|
150
|
+
* @returns {boolean}
|
|
151
|
+
*/
|
|
152
|
+
function isUncPath(token) {
|
|
153
|
+
return token.startsWith('\\\\');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Replace absolute-path tokens in a string with the redaction placeholder.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} input
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
function redactString(input) {
|
|
163
|
+
return input.replace(PATH_TOKEN_RE, (raw) => {
|
|
164
|
+
const match = TRAILING_PUNCT_RE.exec(raw);
|
|
165
|
+
const core = match[1];
|
|
166
|
+
const tail = match[2];
|
|
167
|
+
if (isWindowsDrivePath(core) || isUncPath(core) || looksLikeAbsolutePath(core)) {
|
|
168
|
+
return `${REDACTED_PATH_PLACEHOLDER}${tail}`;
|
|
169
|
+
}
|
|
170
|
+
return raw;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Deep-redact sensitive absolute paths from any error-output value.
|
|
176
|
+
*
|
|
177
|
+
* Strings have absolute-path tokens replaced with `<redacted-path>`;
|
|
178
|
+
* arrays and plain objects are recursed; all other values (numbers,
|
|
179
|
+
* booleans, null, undefined, and non-plain objects such as Buffers, Dates,
|
|
180
|
+
* Maps, Sets, or Error instances) are returned untouched.
|
|
181
|
+
*
|
|
182
|
+
* @param {unknown} value — message string, details object, or nested value.
|
|
183
|
+
* @returns {unknown} Redacted copy (plain objects/arrays are rebuilt;
|
|
184
|
+
* scalars and non-plain objects pass through).
|
|
185
|
+
*/
|
|
186
|
+
export function redactSensitivePaths(value) {
|
|
187
|
+
if (typeof value === 'string') {
|
|
188
|
+
return redactString(value);
|
|
189
|
+
}
|
|
190
|
+
if (Array.isArray(value)) {
|
|
191
|
+
return value.map(redactSensitivePaths);
|
|
192
|
+
}
|
|
193
|
+
if (value !== null && typeof value === 'object') {
|
|
194
|
+
const proto = Object.getPrototypeOf(value);
|
|
195
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
196
|
+
// Buffer, TypedArray, Date, Map, Set, class instances: leave untouched.
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
const out = {};
|
|
200
|
+
for (const [key, val] of Object.entries(value)) {
|
|
201
|
+
out[key] = redactSensitivePaths(val);
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
return value;
|
|
206
|
+
}
|