@iodes/releasekit 0.1.2 → 0.1.4
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 +92 -36
- package/dist/assets.d.ts +10 -0
- package/dist/assets.js +61 -0
- package/dist/cli.js +17 -9
- package/dist/content.d.ts +7 -0
- package/dist/content.js +104 -13
- package/dist/export.d.ts +2 -2
- package/dist/export.js +41 -27
- package/dist/git.d.ts +1 -0
- package/dist/git.js +9 -0
- package/dist/images.d.ts +4 -1
- package/dist/images.js +37 -14
- package/dist/install.js +3 -3
- package/dist/model.d.ts +37 -0
- package/dist/model.js +9 -3
- package/dist/project.d.ts +8 -1
- package/dist/project.js +71 -7
- package/dist/validate.js +11 -5
- package/kit/references/adoption.md +57 -0
- package/kit/references/format.md +20 -4
- package/kit/references/media-sources.md +3 -3
- package/kit/references/theme-pairing.md +38 -1
- package/kit/references/visual-language.md +1 -1
- package/kit/references/workflow.md +98 -34
- package/kit/references/writing.md +36 -5
- package/kit/skills/releasekit-draft/SKILL.md +15 -5
- package/kit/skills/releasekit-finalize/SKILL.md +24 -0
- package/kit/skills/releasekit-image/SKILL.md +11 -5
- package/package.json +1 -1
- package/schemas/config.schema.json +87 -0
- package/schemas/release.schema.json +7 -0
- package/kit/skills/releasekit-review/SKILL.md +0 -16
- package/kit/skills/releasekit-translate/SKILL.md +0 -14
package/README.md
CHANGED
|
@@ -53,23 +53,34 @@ Run inside your product's Git repository:
|
|
|
53
53
|
releasekit init --tools codex,claude,cursor --themes both
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
Choose the tools you use: `codex`, `claude`, `cursor`, or a comma-separated list.
|
|
56
|
+
Choose the tools you use: `codex`, `claude`, `cursor`, or a comma-separated list. New projects use English (`en-US`) as the original language, with no translations enabled until selected. Edit `releasekit/config.yaml` to set your product name, intentional language overrides, and visual settings. Setup prints the skill invocation hints for each tool.
|
|
57
57
|
|
|
58
58
|
### 3. Ask your agent
|
|
59
59
|
|
|
60
|
-
Start with the installed `releasekit-draft` skill. Example conversation with an image tool available:
|
|
60
|
+
Start with the installed `releasekit-draft` skill. Example conversation with a Korean-speaking user, shown here in English, and an image tool available:
|
|
61
61
|
|
|
62
62
|
```text
|
|
63
|
-
You: Use releasekit-draft to draft 1.4.0
|
|
64
|
-
AI:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
You: Korean
|
|
68
|
-
AI:
|
|
69
|
-
|
|
70
|
-
✓
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
You: Use releasekit-draft to draft 1.4.0.
|
|
64
|
+
AI: I'll use English for the original notes. Add Korean, your current
|
|
65
|
+
language, as a translation (recommended), or use English only?
|
|
66
|
+
You can also enter additional languages together.
|
|
67
|
+
You: Korean and Japanese translations.
|
|
68
|
+
AI: Found v1.4.0 and its preceding release tag v1.3.0 on this line.
|
|
69
|
+
Created releasekit/releases/1.4.0/
|
|
70
|
+
✓ Pinned v1.3.0 → v1.4.0 and collected change evidence
|
|
71
|
+
✓ Wrote English originals with Korean and Japanese translations
|
|
72
|
+
Next: use releasekit-image to prepare images for all drafted notes.
|
|
73
|
+
Choose: Prepare images (recommended), Request copy changes,
|
|
74
|
+
or Finish drafting — save the draft and ask for revisions later.
|
|
75
|
+
|
|
76
|
+
You: Finish drafting.
|
|
77
|
+
AI: Drafting 1.4.0 is complete. Review releasekit/releases/1.4.0/.
|
|
78
|
+
Ask me here to rewrite a feature description, leave a feature out,
|
|
79
|
+
or add a feature that was missed. I'll update the draft and its
|
|
80
|
+
affected translations. Images and finalization remain pending.
|
|
81
|
+
|
|
82
|
+
You: Rewrite the queue feature description to explain when to use it.
|
|
83
|
+
AI: Updated that feature description and its Korean and Japanese translations.
|
|
73
84
|
|
|
74
85
|
You: Prepare images.
|
|
75
86
|
AI: ✓ Generated dark/light explanations from shared scene briefs
|
|
@@ -80,39 +91,67 @@ AI: ✓ Generated dark/light explanations from shared scene briefs
|
|
|
80
91
|
|
|
81
92
|
You: Use ./approved-capture.png for the product-detail note.
|
|
82
93
|
AI: ✓ Reviewed and imported it once for both viewer themes
|
|
83
|
-
|
|
94
|
+
Images for all current notes are ready to review.
|
|
95
|
+
Ask me to revise any feature image or replace it with another image.
|
|
96
|
+
If no image changes are needed, use releasekit-finalize to confirm 1.4.0.
|
|
84
97
|
|
|
85
|
-
You:
|
|
98
|
+
You: Use releasekit-finalize for 1.4.0, then export up to three releases
|
|
86
99
|
in English to ./release-output.
|
|
87
100
|
AI: ✓ Validated notes, translations, and images
|
|
88
101
|
✓ Marked release 1.4.0 ready
|
|
89
102
|
Exported release-output/
|
|
90
|
-
├── release-notes.json
|
|
91
|
-
└── assets/
|
|
103
|
+
├── release-notes.en-US.json ← English notes grouped by release
|
|
104
|
+
└── assets/ ← Theme variants and shared supplied images
|
|
92
105
|
```
|
|
93
106
|
|
|
94
|
-
The
|
|
107
|
+
The agent resolves the version and Git range from your request, saved releases, repository tags, and release metadata. It reports a clear scope and proceeds without a tag-selection or confirmation step. If inspection leaves materially different scopes, it asks which work to cover in ordinary language. You can still supply explicit refs to select a particular interval.
|
|
108
|
+
|
|
109
|
+
The draft skill defaults the original language to English and suggests the user's current language for optional translation. Its question accepts additional language names or locale codes through free-text input, as well as an English-only choice. An English-speaking user is not offered a duplicate English translation. Explicit source choices and existing release selections are reused. The selection is saved for that release; future project defaults change only when requested. The draft includes the source and those selected translations. Use the same draft skill later to add a language or refresh translations.
|
|
95
110
|
|
|
96
111
|
Invoke the skill with `$releasekit-draft` in Codex, `/releasekit-draft` in Claude Code, or the skill picker in Cursor. The agent runs the CLI, generates flat explanations, and requests approved source images when the actual product or content must be shown.
|
|
97
112
|
|
|
113
|
+
## Adopting ReleaseKit later
|
|
114
|
+
|
|
115
|
+
You can start after your product has already shipped. Ask the draft skill to set a starting point; it finds a relevant release tag or commit from the repository and asks how to handle the earlier period when that choice is unresolved. Regular notes begin **after** the selected baseline commit.
|
|
116
|
+
|
|
117
|
+
| Earlier history | Result |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| **Product introduction** (recommended for an established product) | A concise overview of capabilities at the baseline, grounded in that snapshot without reconstructing every old commit. |
|
|
120
|
+
| **Analyze history** | Notes based on the repository's beginning through the baseline. |
|
|
121
|
+
| **Skip** | No earlier entry; start recording subsequent changes. |
|
|
122
|
+
|
|
123
|
+
An introduction does not assume that adopting ReleaseKit was the product's launch. Version, date, and product claims should reflect the actual product. The agent reuses choices already made in the conversation or saved setup.
|
|
124
|
+
|
|
125
|
+
For example, introduce the product at `v1.3.0`, then record changes from there:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
releasekit start --at v1.3.0 --past summary --baseline-version 1.3.0
|
|
129
|
+
releasekit prepare 1.3.0
|
|
130
|
+
# Ask the agent to write, review, and finalize the baseline introduction.
|
|
131
|
+
releasekit prepare 1.4.0 --previous 1.3.0 --to v1.4.0
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Use `--past history` to analyze earlier commits instead. With `--past skip`, omit `--baseline-version`; the first regular `prepare` uses the saved boundary. You can save `HEAD` as the start now and prepare the first draft when later commits exist. Setup pins the SHA, so moving a tag or adding commits cannot shift the boundary.
|
|
135
|
+
|
|
136
|
+
`start` saves the choice and creates no notes. Summary/history baselines become ordinary draft releases when prepared and follow the same draft (including translations), image, and finalization workflow. They count as one release in exported history; skipping creates no extra group. Existing releases keep their current workflow. See the [first-use guide](kit/references/adoption.md).
|
|
137
|
+
|
|
98
138
|
## Workflow
|
|
99
139
|
|
|
100
140
|
```text
|
|
101
|
-
Git range →
|
|
141
|
+
Git range → Draft source + translations → Images → Finalize → Optional export
|
|
102
142
|
```
|
|
103
143
|
|
|
104
144
|
| Skill | Purpose |
|
|
105
145
|
| --- | --- |
|
|
106
|
-
| `releasekit-draft` |
|
|
107
|
-
| `releasekit-image` | Plan, generate or request, review, and import images. |
|
|
108
|
-
| `releasekit-
|
|
109
|
-
| `releasekit-review` | Review content, evidence, images, and release readiness. |
|
|
146
|
+
| `releasekit-draft` | Choose a first-use baseline, write and revise source notes and selected translations, or refresh translations alone. |
|
|
147
|
+
| `releasekit-image` | Plan, generate or request, review, and import required images. |
|
|
148
|
+
| `releasekit-finalize` | Review copy, evidence, translations, and images; validate and mark the release ready; export when requested. |
|
|
110
149
|
|
|
111
|
-
All
|
|
150
|
+
All three skills use the agent's native question picker when clarification is needed and the tool is available, with free-text input for another answer; otherwise they ask in chat. This covers language choices, image references, translation scope or terminology, and unresolved finalization/export choices. Existing decisions are reused, and routine technical parameters are resolved through repository inspection. Required CLI flags do not become a questionnaire. Once a question is asked, dependent work waits for your submitted answer; a default selection, elapsed time, or a closed picker does not count as a choice. Only one question request may remain unanswered in the conversation: newly discovered questions and next-step choices wait in a queue, even across skills or releases. A partial answer keeps the remaining questions pending. The agent keeps an asynchronous picker open while waiting, or asks in chat if the environment cannot support that wait. Image uploads are requested through the conversation's attachment flow.
|
|
112
151
|
|
|
113
|
-
After each stage, the agent reports what is complete and recommends the next useful task based on the release's current state. Choose a suggested action or describe another direction to continue in the same conversation; you can
|
|
152
|
+
After each stage, the agent reports what is complete and recommends the next useful task based on the release's current state. Choose a suggested action or describe another direction to continue in the same conversation. After drafting is complete, **Finish drafting** saves the draft for you to read and request changes from the agent. The handoff includes links and examples of revision requests; the agent edits the same draft and refreshes affected translations when you ask. At other stages, you can stop for now. If you already requested the remaining work, the agent continues without asking again. When source and translations are complete, the next step is images; when required images are complete, it is finalization. Text-only releases go directly to finalization. Completed steps are skipped, and missing images or translations stay visible until resolved.
|
|
114
153
|
|
|
115
|
-
The agent handles editorial work, media selection, and image generation where appropriate. The CLI handles files, evidence, validation, and export.
|
|
154
|
+
The agent handles editorial work, media selection, and image generation where appropriate. The CLI handles files, evidence, validation, and export. Finalization includes review and marks local content ready with a content fingerprint. Export is optional; committing, publishing, and displaying it remain separate steps.
|
|
116
155
|
|
|
117
156
|
<details>
|
|
118
157
|
<summary><strong>Step-by-step CLI workflow</strong></summary>
|
|
@@ -121,9 +160,15 @@ Replace the sample version, Git refs, and note ID with your own. If release `1.3
|
|
|
121
160
|
|
|
122
161
|
```sh
|
|
123
162
|
releasekit prepare 1.4.0 --from v1.3.0 --to v1.4.0
|
|
163
|
+
# Save selected languages in release.yaml before adding notes.
|
|
164
|
+
# This example uses sourceLocale: en-US and locales: [en-US, ko-KR].
|
|
124
165
|
releasekit note add 1.4.0 queue-action
|
|
125
166
|
|
|
126
|
-
#
|
|
167
|
+
# Write the source and selected translations, then attach evidence.
|
|
168
|
+
# Review each translation before marking it current.
|
|
169
|
+
releasekit translation mark 1.4.0 queue-action --locale ko-KR
|
|
170
|
+
|
|
171
|
+
# Complete the visual brief for image work.
|
|
127
172
|
# Choose an archetype and set scene.source to generated for this example.
|
|
128
173
|
releasekit image plan 1.4.0
|
|
129
174
|
|
|
@@ -135,20 +180,24 @@ releasekit image plan 1.4.0
|
|
|
135
180
|
# Generate and review the matching light image, then import it.
|
|
136
181
|
releasekit image import 1.4.0 queue-action --theme light --file ./selected-light.png
|
|
137
182
|
|
|
138
|
-
# Review
|
|
139
|
-
releasekit translation mark 1.4.0 queue-action --locale en-US
|
|
183
|
+
# Review facts, copy, translations, and selected images, then finalize.
|
|
140
184
|
releasekit validate 1.4.0
|
|
141
185
|
releasekit finalize 1.4.0
|
|
142
|
-
|
|
186
|
+
|
|
187
|
+
# Export when requested.
|
|
188
|
+
releasekit export --out ./release-output
|
|
143
189
|
```
|
|
144
190
|
|
|
145
191
|
- Preparing creates only `release.yaml` with pinned Git boundaries. The agent reads commit history and relevant file diffs from Git as needed.
|
|
146
192
|
- Use `--from-root` for an explicitly requested full-history first release.
|
|
147
|
-
- `--to` defaults to `HEAD
|
|
193
|
+
- `--to` defaults to the pinned SHA when preparing a saved baseline, and to `HEAD` otherwise; `--previous` can supply the comparison start.
|
|
148
194
|
- Edit existing drafts in place. `prepare` never overwrites them.
|
|
149
|
-
- Use `note add --no-image` for an
|
|
195
|
+
- Notes include images by default. Use `note add --no-image` only for an explicit text-only choice. Adding a note clears any previous `emptyReason`.
|
|
196
|
+
- Use `releasekit note remove <version> <id>` to exclude a draft note. It removes the note folder, translations, visual brief, prompts, and unused managed images, including older imports. Images referenced by remaining visuals and source originals are preserved. The result lists removed paths and retained shared assets. Removing the last note leaves the draft pending until you add notes or supply a factual `emptyReason`.
|
|
150
197
|
- Add `--json` for structured results or `--cwd` to select a project directory.
|
|
151
|
-
-
|
|
198
|
+
- Only `--out` is required for export. Omit `--current` to select the release with no successor in the saved `previous` links; multiple release lines require an explicit `--current`. The selected release must be ready. Omit `--limit` to use `history.limit` from `releasekit/config.yaml` (initially 3).
|
|
199
|
+
- Omit `--locale` to export every locale saved in the current release as `release-notes.<locale>.json`, such as `release-notes.en-US.json` and `release-notes.ko-KR.json`. Add `--locale en-US` for only the English file. All files share the same `assets/` directory. Each selected version must contain the requested locales; missing or stale translations stop the export before output is created.
|
|
200
|
+
- Export to a new directory; an existing destination is never overwritten. The command result lists generated JSON paths in `files`, the number of version groups in `releases`, and the number of shared image files in `assets`.
|
|
152
201
|
|
|
153
202
|
See the [agent workflow](kit/references/workflow.md) for ancestry rules and continuing existing releases.
|
|
154
203
|
|
|
@@ -165,6 +214,10 @@ Codex and Cursor share `.agents/skills` to avoid duplicate discovery. Claude Cod
|
|
|
165
214
|
|
|
166
215
|
## Image themes
|
|
167
216
|
|
|
217
|
+
Image work covers **every drafted note** by default, including small fixes and improvements. Only an explicit text-only choice omits a note's image. Calling `releasekit-image` again fills missing images, including those for notes added later, and reuses existing valid images. Existing images that need corrections stay pending until the affected revision or replacement is requested.
|
|
218
|
+
|
|
219
|
+
After generation, review the images and ask the agent to revise anything you dislike or replace it with another approved image. When no image changes are needed and the content is complete, ask for `releasekit-finalize` to confirm the release.
|
|
220
|
+
|
|
168
221
|
**Generated graphics default to dark and light.** Paired variants share one scene brief, preserving geometry, feature meaning, and semantic colors while presentation surfaces adapt. Images are shared across locales.
|
|
169
222
|
|
|
170
223
|
Choose `scene.source` in each note's visual brief before planning:
|
|
@@ -184,6 +237,8 @@ visuals:
|
|
|
184
237
|
|
|
185
238
|
`releasekit image plan <version>` reports pending and reusable assets, with separate `generationRequests` and `providedRequests` counts. It reuses current imports and never calls a model API. Single-theme exports contain one real asset and an explicit fallback.
|
|
186
239
|
|
|
240
|
+
To replace or regenerate an image, import the reviewed result with the same release version and note ID and the intended `--theme`. Import automatically switches shared/themed usage and removes the note's unused managed images after saving, including older imports. Files still referenced by visuals and source originals outside the note's managed assets are preserved. Keep existing variant entries until the replacement is imported. Use `--source provided` when replacing generated illustrations with a supplied image, or `--source generated` for the reverse change when the subject permits it; source and selection are saved together. See [image transitions](kit/references/theme-pairing.md#switch-between-shared-and-themed-images).
|
|
241
|
+
|
|
187
242
|
Requests with `action: generate` include a prompt for the agent or an external tool. Requests with `action: provide` have `promptFile: null` and identify the needed source. Missing supplied media stays pending and blocks finalization; the agent can continue independent writing and translation work.
|
|
188
243
|
|
|
189
244
|
<details>
|
|
@@ -200,7 +255,7 @@ releasekit image import 1.4.0 product-detail --theme shared --file ./approved-ca
|
|
|
200
255
|
|
|
201
256
|
The CLI keeps the original bytes, dimensions, and colors. One `shared` asset serves both viewer themes without generating or duplicating another file.
|
|
202
257
|
|
|
203
|
-
If genuine dark/light captures exist, import them with `--theme dark` and `--theme light` instead.
|
|
258
|
+
If genuine dark/light captures exist, import them with `--theme dark` and `--theme light` instead. The CLI replaces the previous shared/themed selection during import; keep its metadata in place until the new file is successfully registered. A missing configured capture remains a supplied-image request.
|
|
204
259
|
|
|
205
260
|
Follow the current image plan even if older prompt files remain. See the [supplied-media example](examples/provided-media/README.md) and [media source guide](kit/references/media-sources.md) for pending inputs and older briefs.
|
|
206
261
|
|
|
@@ -235,7 +290,7 @@ releasekit/
|
|
|
235
290
|
└── assets/ # Selected raster images
|
|
236
291
|
```
|
|
237
292
|
|
|
238
|
-
Releases store the comparison start and end SHAs in `release.yaml`, with relevant paths or commits attached to individual notes. They do not save a full patch or a separate changed-file index. Draft validation reads the pinned Git range; finalized releases can be validated and exported without Git history.
|
|
293
|
+
Releases store the comparison start and end SHAs in `release.yaml`, with relevant paths or commits attached to individual notes. They do not save a full patch or a separate changed-file index. Draft validation reads the pinned Git range, or the baseline snapshot for a product introduction; finalized releases can be validated and exported without Git history.
|
|
239
294
|
|
|
240
295
|
Export follows explicit `previous` links, keeping each version's notes in a separate group. The default limit is **three releases, including the current one**. Similar notes in different versions remain separate.
|
|
241
296
|
|
|
@@ -249,7 +304,8 @@ Translations track source fingerprints, and finalized releases record content fi
|
|
|
249
304
|
|
|
250
305
|
| Guide | What it covers |
|
|
251
306
|
| --- | --- |
|
|
252
|
-
| [
|
|
307
|
+
| [First use in an existing product](kit/references/adoption.md) | Starting points, product introductions, historical analysis, and skipped history. |
|
|
308
|
+
| [Agent workflow](kit/references/workflow.md) | Git boundaries, drafts and translations, images, finalization, and export. |
|
|
253
309
|
| [Writing and translation](kit/references/writing.md) | Product copy, evidence, and locale freshness. |
|
|
254
310
|
| [Visual language](kit/references/visual-language.md) | Composition, hierarchy, materials, and acceptance checks. |
|
|
255
311
|
| [Choosing generated or supplied media](kit/references/media-sources.md) | Source selection, pending captures, and shared assets. |
|
|
@@ -274,7 +330,7 @@ npm run build
|
|
|
274
330
|
npm pack --dry-run
|
|
275
331
|
```
|
|
276
332
|
|
|
277
|
-
CI runs on Windows and Linux with Node.js 22 and 24. Tests cover Git ranges, release history, image integrity, theme policies, supplied media and shared assets, translation freshness, finalization, installation conflicts, and CLI behavior.
|
|
333
|
+
CI runs on Windows and Linux with Node.js 22 and 24. Tests cover first-use setup, snapshot summaries, Git ranges, release history, image integrity, theme policies, supplied media and shared assets, translation freshness, finalization, installation conflicts, and CLI behavior.
|
|
278
334
|
|
|
279
335
|
<details>
|
|
280
336
|
<summary><strong>Install from a local checkout</strong></summary>
|
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type AssetVariant, type Visual } from './model.js';
|
|
2
|
+
import { Project } from './project.js';
|
|
3
|
+
export declare function fileKey(file: string): Promise<string>;
|
|
4
|
+
export declare function visualFileReferences(project: Project, version: string, file: string, selected?: Visual): Promise<Set<string>>;
|
|
5
|
+
export declare function retainedImageFiles(project: Project, replacement: {
|
|
6
|
+
version: string;
|
|
7
|
+
noteId: string;
|
|
8
|
+
visual: Visual | null;
|
|
9
|
+
}): Promise<Set<string>>;
|
|
10
|
+
export declare function managedAssetFiles(project: Project, version: string, noteId?: string, variant?: AssetVariant): Promise<string[]>;
|
package/dist/assets.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import { visualSchema } from './model.js';
|
|
3
|
+
import { Project } from './project.js';
|
|
4
|
+
import { exists, identifier, readYaml, within } from './files.js';
|
|
5
|
+
// References remain readable while a draft's scene brief is unfinished.
|
|
6
|
+
const imageReferencesSchema = visualSchema.pick({ variants: true }).extend({
|
|
7
|
+
scene: visualSchema.shape.scene.pick({ references: true }).strip(),
|
|
8
|
+
}).strip();
|
|
9
|
+
export async function fileKey(file) {
|
|
10
|
+
const resolved = await exists(file) ? await fs.realpath(file) : file;
|
|
11
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
12
|
+
}
|
|
13
|
+
export async function visualFileReferences(project, version, file, selected) {
|
|
14
|
+
const visual = selected ?? await readYaml(file, imageReferencesSchema);
|
|
15
|
+
const references = new Set();
|
|
16
|
+
for (const asset of Object.values(visual.variants)) {
|
|
17
|
+
if (asset)
|
|
18
|
+
references.add(await fileKey(await project.releaseFile(version, asset.file)));
|
|
19
|
+
}
|
|
20
|
+
for (const reference of visual.scene.references)
|
|
21
|
+
references.add(await fileKey(await within(project.root, reference)));
|
|
22
|
+
return references;
|
|
23
|
+
}
|
|
24
|
+
export async function retainedImageFiles(project, replacement) {
|
|
25
|
+
const retained = new Set();
|
|
26
|
+
for (const version of await project.versions()) {
|
|
27
|
+
const directory = await project.releaseFile(version, 'visuals');
|
|
28
|
+
if (!(await exists(directory)))
|
|
29
|
+
continue;
|
|
30
|
+
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
|
|
31
|
+
if (entry.isDirectory() || !entry.name.endsWith('.yaml'))
|
|
32
|
+
continue;
|
|
33
|
+
const replacing = version === replacement.version && entry.name === `${replacement.noteId}.yaml`;
|
|
34
|
+
if (replacing && replacement.visual === null)
|
|
35
|
+
continue;
|
|
36
|
+
const file = await project.releaseFile(version, `visuals/${entry.name}`);
|
|
37
|
+
for (const reference of await visualFileReferences(project, version, file, replacing ? replacement.visual : undefined)) {
|
|
38
|
+
retained.add(reference);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return retained;
|
|
43
|
+
}
|
|
44
|
+
export async function managedAssetFiles(project, version, noteId, variant) {
|
|
45
|
+
if (noteId !== undefined)
|
|
46
|
+
identifier(noteId);
|
|
47
|
+
const directory = await project.releaseFile(version, 'assets');
|
|
48
|
+
if (!(await exists(directory)))
|
|
49
|
+
return [];
|
|
50
|
+
const prefix = noteId === undefined ? '' : `${noteId}.${variant ? `${variant}.` : ''}`;
|
|
51
|
+
const suffix = noteId === undefined
|
|
52
|
+
? /^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,95}\.(dark|light|shared)\.[a-f0-9]{12}\.(png|jpg|webp)$/
|
|
53
|
+
: variant ? /^[a-f0-9]{12}\.(png|jpg|webp)$/ : /^(dark|light|shared)\.[a-f0-9]{12}\.(png|jpg|webp)$/;
|
|
54
|
+
const files = [];
|
|
55
|
+
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
|
|
56
|
+
if (entry.isFile() && entry.name.startsWith(prefix) && suffix.test(entry.name.slice(prefix.length))) {
|
|
57
|
+
files.push(await project.releaseFile(version, `assets/${entry.name}`));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return files.sort();
|
|
61
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { Command, Option } from 'commander';
|
|
5
|
-
import { Project, prepare } from './project.js';
|
|
5
|
+
import { Project, prepare, startProject } from './project.js';
|
|
6
6
|
import { initProject, installSkills } from './install.js';
|
|
7
|
-
import { addNote, markTranslation, syncImagePolicy } from './content.js';
|
|
7
|
+
import { addNote, removeNote, markTranslation, syncImagePolicy } from './content.js';
|
|
8
8
|
import { planImages, importImage } from './images.js';
|
|
9
9
|
import { validate, finalize } from './validate.js';
|
|
10
10
|
import { exportBundle } from './export.js';
|
|
@@ -29,9 +29,14 @@ program.command('init').description('Initialize content and install project skil
|
|
|
29
29
|
program.command('update').description('Refresh managed skills while preserving user edits')
|
|
30
30
|
.action(async () => { const result = await installSkills(project()); emit(result); if (result.conflicts.length)
|
|
31
31
|
process.exitCode = 1; });
|
|
32
|
+
program.command('start').description('Save the first-use Git boundary and treatment of earlier history')
|
|
33
|
+
.requiredOption('--at <ref>', 'baseline commit or tag; subsequent notes begin after this commit')
|
|
34
|
+
.addOption(new Option('--past <mode>', 'summarize the baseline, analyze history, or skip earlier notes').choices(['summary', 'history', 'skip']).makeOptionMandatory())
|
|
35
|
+
.option('--baseline-version <version>', 'baseline release ID, required for summary or history')
|
|
36
|
+
.action(async (options) => emit(await startProject(project(), { at: options.at, past: options.past, version: options.baselineVersion })));
|
|
32
37
|
program.command('prepare <version>').description('Create a draft from pinned Git commits')
|
|
33
38
|
.option('--from <ref>', 'comparison start commit or tag')
|
|
34
|
-
.option('--to <ref>', 'comparison end commit or tag
|
|
39
|
+
.option('--to <ref>', 'comparison end commit or tag, defaults to the saved baseline or HEAD')
|
|
35
40
|
.option('--previous <version>', 'explicit previous release')
|
|
36
41
|
.option('--from-root', 'explicitly include the whole history')
|
|
37
42
|
.option('--first-release', 'start an independent release line')
|
|
@@ -45,6 +50,8 @@ note.command('add <version> <id>').description('Scaffold a note and its locale f
|
|
|
45
50
|
await addNote(project(), version, id, noteMetaSchema.shape.category.parse(options.category), options.image);
|
|
46
51
|
emit({ version, note: id, status: 'draft' });
|
|
47
52
|
});
|
|
53
|
+
note.command('remove <version> <id>').description('Remove a draft note, its locale files, prompts, and unused managed images')
|
|
54
|
+
.action(async (version, id) => emit(await removeNote(project(), version, id)));
|
|
48
55
|
const images = program.command('image').description('Plan themed illustrations and register selected files');
|
|
49
56
|
images.command('plan <version>').description('Plan generation or supplied-image requests without calling a model')
|
|
50
57
|
.option('--sync-config', 'apply current project image settings to this draft')
|
|
@@ -54,10 +61,11 @@ images.command('plan <version>').description('Plan generation or supplied-image
|
|
|
54
61
|
await syncImagePolicy(instance, version);
|
|
55
62
|
emit(await planImages(instance, version));
|
|
56
63
|
});
|
|
57
|
-
images.command('import <version> <note>').description('Import
|
|
58
|
-
.addOption(new Option('--theme <theme>', 'variant to register; shared
|
|
64
|
+
images.command('import <version> <note>').description('Import or replace an image, switch shared/themed usage, and remove unused note images')
|
|
65
|
+
.addOption(new Option('--theme <theme>', 'variant to register; shared replaces themed entries and a theme replaces shared').choices(['dark', 'light', 'shared']).makeOptionMandatory())
|
|
66
|
+
.addOption(new Option('--source <source>', 'save the media source with this import; otherwise keep the current source').choices(['generated', 'provided']))
|
|
59
67
|
.requiredOption('--file <file>', 'selected local PNG, JPEG, or WebP')
|
|
60
|
-
.action(async (version, id, options) => emit(await importImage(project(), version, id, assetVariant.parse(options.theme), path.resolve(program.opts().cwd, options.file))));
|
|
68
|
+
.action(async (version, id, options) => emit(await importImage(project(), version, id, assetVariant.parse(options.theme), path.resolve(program.opts().cwd, options.file), { source: options.source })));
|
|
61
69
|
const translation = program.command('translation').description('Track source freshness for reviewed translations');
|
|
62
70
|
translation.command('mark <version> <note>').description('Mark an already reviewed translation current')
|
|
63
71
|
.requiredOption('--locale <locale>', 'translation language code')
|
|
@@ -74,9 +82,9 @@ program.command('validate [version]').description('Validate one release or all r
|
|
|
74
82
|
program.command('finalize <version>').description('Validate and mark local release content ready')
|
|
75
83
|
.action(async (version) => emit(await finalize(project(), version)));
|
|
76
84
|
program.command('export').description('Export recent version groups and selected image variants')
|
|
77
|
-
.
|
|
78
|
-
.option('--limit <count>', 'number of version groups, including current', value => Number(value))
|
|
79
|
-
.option('--locale <locale>', 'output language, defaults to the
|
|
85
|
+
.option('--current <version>', 'current release version, defaults to the release with no successor')
|
|
86
|
+
.option('--limit <count>', 'number of version groups, including current, defaults to project history.limit (initially 3)', value => Number(value))
|
|
87
|
+
.option('--locale <locale>', 'output language, defaults to all locales saved in the current release')
|
|
80
88
|
.requiredOption('--out <directory>', 'new output directory')
|
|
81
89
|
.action(async (options) => emit(await exportBundle(project(), options.current, { ...options, out: path.resolve(program.opts().cwd, options.out) })));
|
|
82
90
|
async function main() {
|
package/dist/content.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { type NoteMeta } from './model.js';
|
|
2
2
|
import { Project } from './project.js';
|
|
3
3
|
export declare function addNote(project: Project, version: string, id: string, category: NoteMeta['category'], image: boolean): Promise<void>;
|
|
4
|
+
export declare function removeNote(project: Project, version: string, id: string): Promise<{
|
|
5
|
+
version: string;
|
|
6
|
+
note: string;
|
|
7
|
+
status: "draft" | "ready";
|
|
8
|
+
removedPaths: string[];
|
|
9
|
+
retainedAssets: string[];
|
|
10
|
+
}>;
|
|
4
11
|
export declare function markTranslation(project: Project, version: string, id: string, language: string): Promise<string>;
|
|
5
12
|
export declare function syncImagePolicy(project: Project, version: string): Promise<void>;
|
|
6
13
|
export declare function readVisual(project: Project, version: string, id: string): Promise<{
|
package/dist/content.js
CHANGED
|
@@ -1,28 +1,119 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileKey, managedAssetFiles, retainedImageFiles, visualFileReferences } from './assets.js';
|
|
2
4
|
import { locale, noteMetaSchema, visualSchema, imageSource } from './model.js';
|
|
3
5
|
import { identifier, writeYaml, writeNote, readNote, noteHash, exists, readYaml } from './files.js';
|
|
4
6
|
import { Project, editable } from './project.js';
|
|
7
|
+
async function notePaths(project, version, id) {
|
|
8
|
+
return Promise.all([
|
|
9
|
+
`notes/${id}`, `visuals/${id}.yaml`,
|
|
10
|
+
...['dark', 'light', 'shared'].map(variant => `prompts/${id}.${variant}.md`),
|
|
11
|
+
].map(relative => project.releaseFile(version, relative)));
|
|
12
|
+
}
|
|
5
13
|
export async function addNote(project, version, id, category, image) {
|
|
6
14
|
const release = await project.release(version);
|
|
7
15
|
editable(release);
|
|
8
16
|
identifier(id);
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
const paths = await notePaths(project, version, id);
|
|
18
|
+
if (release.notes.some(note => note.id === id) || (await Promise.all(paths.map(exists))).some(Boolean)) {
|
|
19
|
+
throw new Error(`Note ${id} already exists or has leftover files; its content was preserved.`);
|
|
11
20
|
}
|
|
12
21
|
const note = noteMetaSchema.parse({ id, category, commits: [], paths: [], image });
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
const directory = paths[0], visualFile = paths[1];
|
|
23
|
+
await fs.mkdir(path.dirname(directory), { recursive: true });
|
|
24
|
+
await fs.mkdir(directory);
|
|
25
|
+
let createdVisual = false;
|
|
26
|
+
try {
|
|
27
|
+
for (const language of release.locales) {
|
|
28
|
+
await writeNote(await project.releaseFile(version, `notes/${id}/${language}.md`), { title: '', alt: '', sourceHash: null, body: '' });
|
|
29
|
+
}
|
|
30
|
+
if (image) {
|
|
31
|
+
await writeYaml(visualFile, {
|
|
32
|
+
schemaVersion: 1,
|
|
33
|
+
scene: { archetype: '', source: '', subject: '', message: '', focus: '', composition: '', context: '', elements: [], preserve: [], avoid: [], text: [], references: [] },
|
|
34
|
+
variants: {},
|
|
35
|
+
});
|
|
36
|
+
createdVisual = true;
|
|
37
|
+
}
|
|
38
|
+
release.notes.push(note);
|
|
39
|
+
release.emptyReason = null;
|
|
40
|
+
release.contentHash = null;
|
|
41
|
+
await project.save(release);
|
|
15
42
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
});
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (createdVisual)
|
|
45
|
+
await fs.unlink(visualFile);
|
|
46
|
+
await fs.rm(await project.releaseFile(version, `notes/${id}`), { recursive: true, force: true });
|
|
47
|
+
throw error;
|
|
22
48
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
await project.
|
|
49
|
+
}
|
|
50
|
+
export async function removeNote(project, version, id) {
|
|
51
|
+
const release = await project.release(version);
|
|
52
|
+
editable(release);
|
|
53
|
+
identifier(id);
|
|
54
|
+
if (!release.notes.some(note => note.id === id))
|
|
55
|
+
throw new Error(`Unknown note: ${id}`);
|
|
56
|
+
const paths = await notePaths(project, version, id);
|
|
57
|
+
const directory = await project.releaseDir(version);
|
|
58
|
+
const visualFile = paths[1];
|
|
59
|
+
const references = await exists(visualFile) ? await visualFileReferences(project, version, visualFile) : new Set();
|
|
60
|
+
const retained = await retainedImageFiles(project, { version, noteId: id, visual: null });
|
|
61
|
+
const owned = new Set(await managedAssetFiles(project, version, id));
|
|
62
|
+
const retainedAssets = [];
|
|
63
|
+
for (const file of await managedAssetFiles(project, version)) {
|
|
64
|
+
const key = await fileKey(file);
|
|
65
|
+
if (!owned.has(file) && !references.has(key))
|
|
66
|
+
continue;
|
|
67
|
+
if (retained.has(key))
|
|
68
|
+
retainedAssets.push(file);
|
|
69
|
+
else
|
|
70
|
+
paths.push(file);
|
|
71
|
+
}
|
|
72
|
+
const targets = [];
|
|
73
|
+
for (const file of paths) {
|
|
74
|
+
if (!(await exists(file)))
|
|
75
|
+
continue;
|
|
76
|
+
const key = await fileKey(file);
|
|
77
|
+
if ([...retained].some(reference => reference === key || reference.startsWith(`${key}${path.sep}`))) {
|
|
78
|
+
throw new Error(`Note files are referenced by another visual; update those references before removing ${id}: ${file}`);
|
|
79
|
+
}
|
|
80
|
+
targets.push(file);
|
|
81
|
+
}
|
|
82
|
+
// Hold files inside this release until its metadata is saved, so a failed save can restore them.
|
|
83
|
+
const holding = await fs.mkdtemp(await project.releaseFile(version, `.remove-${id}-`));
|
|
84
|
+
const checkedHolding = await project.releaseFile(version, path.basename(holding));
|
|
85
|
+
const moved = [];
|
|
86
|
+
try {
|
|
87
|
+
for (const [index, original] of targets.entries()) {
|
|
88
|
+
const temporary = path.join(checkedHolding, String(index));
|
|
89
|
+
await fs.rename(original, temporary);
|
|
90
|
+
moved.push({ original, temporary });
|
|
91
|
+
}
|
|
92
|
+
await project.save({ ...release, notes: release.notes.filter(note => note.id !== id), contentHash: null });
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
const failures = [];
|
|
96
|
+
for (const entry of moved.reverse()) {
|
|
97
|
+
try {
|
|
98
|
+
await fs.rename(entry.temporary, entry.original);
|
|
99
|
+
}
|
|
100
|
+
catch (restoreError) {
|
|
101
|
+
failures.push(restoreError);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (failures.length)
|
|
105
|
+
throw new AggregateError([error, ...failures], `Note removal failed; recovery files remain in ${checkedHolding}`);
|
|
106
|
+
await fs.rmdir(checkedHolding);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
await fs.rm(await project.releaseFile(version, path.basename(checkedHolding)), { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
throw new Error(`Note ${id} was removed, but file cleanup is incomplete in ${checkedHolding}`, { cause: error });
|
|
114
|
+
}
|
|
115
|
+
const relative = (file) => path.relative(directory, file).split(path.sep).join('/');
|
|
116
|
+
return { version, note: id, status: release.status, removedPaths: targets.map(relative), retainedAssets: retainedAssets.map(relative) };
|
|
26
117
|
}
|
|
27
118
|
export async function markTranslation(project, version, id, language) {
|
|
28
119
|
const release = await project.release(version);
|
package/dist/export.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Project } from './project.js';
|
|
2
|
-
export declare function exportBundle(project: Project, current: string, options: {
|
|
2
|
+
export declare function exportBundle(project: Project, current: string | undefined, options: {
|
|
3
3
|
limit?: number;
|
|
4
4
|
locale?: string;
|
|
5
5
|
out: string;
|
|
6
6
|
}): Promise<{
|
|
7
|
-
|
|
7
|
+
files: string[];
|
|
8
8
|
releases: number;
|
|
9
9
|
assets: number;
|
|
10
10
|
}>;
|