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.
Files changed (71) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +2 -2
  4. package/CHANGELOG.md +104 -0
  5. package/INSTALL.md +81 -1
  6. package/INSTALL.zh-CN.md +69 -1
  7. package/README.md +233 -8
  8. package/README.zh-CN.md +188 -8
  9. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  10. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  11. package/adapters/claude/bin/release-skill.bundle.mjs +14164 -9912
  12. package/adapters/claude/bin/release-skill.mjs +24 -4
  13. package/adapters/claude/native/safe-write/binding.gyp +2 -1
  14. package/adapters/claude/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
  15. package/adapters/claude/native/safe-write/prebuilds.json +1 -1
  16. package/adapters/claude/schemas/.render-manifest.json +10 -10
  17. package/adapters/claude/schemas/release-project.schema.json +141 -0
  18. package/adapters/claude/skills/release-help/SKILL.md +21 -0
  19. package/adapters/claude/skills/release-prepare/SKILL.md +17 -6
  20. package/adapters/claude/skills/release-publish/SKILL.md +3 -1
  21. package/adapters/claude/skills/release-reconcile/SKILL.md +1 -1
  22. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  23. package/adapters/codex/bin/release-skill.bundle.mjs +14164 -9912
  24. package/adapters/codex/bin/release-skill.mjs +24 -4
  25. package/adapters/codex/native/safe-write/binding.gyp +2 -1
  26. package/adapters/codex/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
  27. package/adapters/codex/native/safe-write/prebuilds.json +1 -1
  28. package/adapters/codex/schemas/.render-manifest.json +10 -10
  29. package/adapters/codex/schemas/release-project.schema.json +141 -0
  30. package/adapters/codex/skills/release-help/SKILL.md +21 -0
  31. package/adapters/codex/skills/release-prepare/SKILL.md +17 -6
  32. package/adapters/codex/skills/release-publish/SKILL.md +3 -1
  33. package/adapters/codex/skills/release-reconcile/SKILL.md +1 -1
  34. package/bin/release-skill-cli.mjs +163 -4
  35. package/bin/release-skill.bundle.mjs +14164 -9912
  36. package/bin/release-skill.mjs +24 -4
  37. package/native/safe-write/binding.gyp +2 -1
  38. package/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
  39. package/native/safe-write/prebuilds.json +1 -1
  40. package/package.json +2 -2
  41. package/references/.render-manifest.json +4 -4
  42. package/references/02-project-config.md +24 -0
  43. package/references/05-evidence-and-errors.md +5 -0
  44. package/schemas/.render-manifest.json +10 -10
  45. package/schemas/release-project.schema.json +141 -0
  46. package/scripts/build-bundle.mjs +15 -2
  47. package/skills/release-help/SKILL.md +21 -0
  48. package/skills/release-prepare/SKILL.md +17 -6
  49. package/skills/release-publish/SKILL.md +3 -1
  50. package/skills/release-reconcile/SKILL.md +1 -1
  51. package/skills-src/release-help/SKILL.md +21 -0
  52. package/skills-src/release-prepare/SKILL.md +17 -6
  53. package/skills-src/release-publish/SKILL.md +3 -1
  54. package/skills-src/release-reconcile/SKILL.md +1 -1
  55. package/src/adapters/plugin-marketplace.mjs +70 -3
  56. package/src/artifacts/transaction-journal.mjs +1126 -105
  57. package/src/artifacts/transaction.mjs +313 -130
  58. package/src/commands/docs.mjs +332 -0
  59. package/src/commands/prepare.mjs +324 -17
  60. package/src/commands/reconcile.mjs +4 -1
  61. package/src/commands/verify.mjs +4 -1
  62. package/src/core/errors.mjs +64 -2
  63. package/src/core/plan.mjs +59 -1
  64. package/src/core/redact.mjs +206 -0
  65. package/src/docs/changelog-renderer.mjs +853 -0
  66. package/src/docs/config.mjs +337 -0
  67. package/src/docs/notes-loader.mjs +432 -0
  68. package/src/docs/notes.mjs +553 -0
  69. package/src/docs/readme-renderer.mjs +647 -0
  70. package/src/docs/refresh-planner.mjs +542 -0
  71. 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
+ }