@iodes/releasekit 0.1.3 → 0.1.5
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 +49 -21
- package/dist/assets.d.ts +10 -0
- package/dist/assets.js +61 -0
- package/dist/cli.js +10 -7
- 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/images.d.ts +4 -1
- package/dist/images.js +37 -14
- package/dist/project.d.ts +1 -0
- package/dist/project.js +13 -0
- package/dist/prompts.js +8 -8
- package/examples/README.md +3 -1
- package/examples/location-preferences/README.md +5 -2
- package/examples/location-preferences/dark-neutral.png +0 -0
- package/examples/location-preferences/dark.prompt.md +10 -8
- package/examples/location-preferences/light-neutral.png +0 -0
- package/examples/location-preferences/light.prompt.md +10 -8
- package/examples/location-preferences/neutral-edit-requests.md +21 -0
- package/examples/location-preferences/pair-review.md +9 -8
- package/examples/location-preferences/scene.yaml +9 -6
- package/kit/references/common-images.md +56 -0
- package/kit/references/composition-recipes.md +3 -3
- package/kit/references/format.md +18 -2
- package/kit/references/media-sources.md +5 -3
- package/kit/references/theme-pairing.md +41 -4
- package/kit/references/visual-language.md +12 -6
- package/kit/references/workflow.md +47 -21
- package/kit/references/writing.md +101 -9
- package/kit/skills/releasekit-draft/SKILL.md +9 -3
- package/kit/skills/releasekit-finalize/SKILL.md +8 -2
- package/kit/skills/releasekit-image/SKILL.md +13 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,11 +53,11 @@ 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. 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,
|
|
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, language defaults for new drafts, 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 a Korean-speaking user, shown here in English, and an image tool available:
|
|
60
|
+
Start with the installed `releasekit-draft` skill. Example first-use conversation with a Korean-speaking user, shown here in English, and an image tool available:
|
|
61
61
|
|
|
62
62
|
```text
|
|
63
63
|
You: Use releasekit-draft to draft 1.4.0.
|
|
@@ -65,12 +65,23 @@ AI: I'll use English for the original notes. Add Korean, your current
|
|
|
65
65
|
language, as a translation (recommended), or use English only?
|
|
66
66
|
You can also enter additional languages together.
|
|
67
67
|
You: Korean and Japanese translations.
|
|
68
|
-
AI:
|
|
68
|
+
AI: Saved these language defaults in releasekit/config.yaml.
|
|
69
|
+
Found v1.4.0 and its preceding release tag v1.3.0 on this line.
|
|
69
70
|
Created releasekit/releases/1.4.0/
|
|
70
71
|
✓ Pinned v1.3.0 → v1.4.0 and collected change evidence
|
|
71
72
|
✓ Wrote English originals with Korean and Japanese translations
|
|
72
|
-
Next: use releasekit-image
|
|
73
|
-
Choose: Prepare images (recommended),
|
|
73
|
+
Next: use releasekit-image to prepare images for all drafted notes.
|
|
74
|
+
Choose: Prepare images (recommended), Request copy changes,
|
|
75
|
+
or Finish drafting — save the draft and ask for revisions later.
|
|
76
|
+
|
|
77
|
+
You: Finish drafting.
|
|
78
|
+
AI: Drafting 1.4.0 is complete. Review releasekit/releases/1.4.0/.
|
|
79
|
+
Ask me here to rewrite a feature description, leave a feature out,
|
|
80
|
+
or add a feature that was missed. I'll update the draft and its
|
|
81
|
+
affected translations. Images and finalization remain pending.
|
|
82
|
+
|
|
83
|
+
You: Rewrite the queue feature description to explain when to use it.
|
|
84
|
+
AI: Updated that feature description and its Korean and Japanese translations.
|
|
74
85
|
|
|
75
86
|
You: Prepare images.
|
|
76
87
|
AI: ✓ Generated dark/light explanations from shared scene briefs
|
|
@@ -81,20 +92,22 @@ AI: ✓ Generated dark/light explanations from shared scene briefs
|
|
|
81
92
|
|
|
82
93
|
You: Use ./approved-capture.png for the product-detail note.
|
|
83
94
|
AI: ✓ Reviewed and imported it once for both viewer themes
|
|
84
|
-
|
|
95
|
+
Images for all current notes are ready to review.
|
|
96
|
+
Ask me to revise any feature image or replace it with another image.
|
|
97
|
+
If no image changes are needed, use releasekit-finalize to confirm 1.4.0.
|
|
85
98
|
|
|
86
99
|
You: Use releasekit-finalize for 1.4.0, then export up to three releases
|
|
87
100
|
in English to ./release-output.
|
|
88
101
|
AI: ✓ Validated notes, translations, and images
|
|
89
102
|
✓ Marked release 1.4.0 ready
|
|
90
103
|
Exported release-output/
|
|
91
|
-
├── release-notes.json
|
|
92
|
-
└── assets/
|
|
104
|
+
├── release-notes.en-US.json ← English notes grouped by release
|
|
105
|
+
└── assets/ ← Theme variants and shared supplied images
|
|
93
106
|
```
|
|
94
107
|
|
|
95
108
|
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.
|
|
96
109
|
|
|
97
|
-
|
|
110
|
+
Language selection is a first-use decision. When it is still unresolved, the draft skill defaults the original to English, suggests the user's current language for optional translation, and accepts additional language names or locale codes as well as an English-only choice. Explicit choices and intentional project settings are reused. The first selection is saved in `releasekit/config.yaml` before preparing the draft, without a separate confirmation. Each new draft uses the current config's original language and complete translation set without asking again, even when previous releases used different languages. Existing drafts keep their saved selection. The draft includes the source and all selected translations; use the same skill to request a language change or refresh translations. Changing project defaults affects future drafts while earlier releases remain unchanged.
|
|
98
111
|
|
|
99
112
|
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.
|
|
100
113
|
|
|
@@ -135,21 +148,21 @@ Git range → Draft source + translations → Images → Finalize → Optional e
|
|
|
135
148
|
| `releasekit-image` | Plan, generate or request, review, and import required images. |
|
|
136
149
|
| `releasekit-finalize` | Review copy, evidence, translations, and images; validate and mark the release ready; export when requested. |
|
|
137
150
|
|
|
138
|
-
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. 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.
|
|
151
|
+
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.
|
|
139
152
|
|
|
140
|
-
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
|
|
153
|
+
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.
|
|
141
154
|
|
|
142
155
|
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.
|
|
143
156
|
|
|
144
157
|
<details>
|
|
145
158
|
<summary><strong>Step-by-step CLI workflow</strong></summary>
|
|
146
159
|
|
|
147
|
-
Replace the sample version, Git refs, and note ID with your own. If release `1.3.0` already exists in ReleaseKit, add `--previous 1.3.0` to link its history.
|
|
160
|
+
Replace the sample version, Git refs, and note ID with your own. Save first-use language choices in `releasekit/config.yaml` before preparation. If release `1.3.0` already exists in ReleaseKit, add `--previous 1.3.0` to link its history.
|
|
148
161
|
|
|
149
162
|
```sh
|
|
150
163
|
releasekit prepare 1.4.0 --from v1.3.0 --to v1.4.0
|
|
151
|
-
#
|
|
152
|
-
# This example
|
|
164
|
+
# prepare copied sourceLocale and locales from the current config.yaml.
|
|
165
|
+
# This example has sourceLocale: en-US and locales: [en-US, ko-KR].
|
|
153
166
|
releasekit note add 1.4.0 queue-action
|
|
154
167
|
|
|
155
168
|
# Write the source and selected translations, then attach evidence.
|
|
@@ -171,17 +184,21 @@ releasekit image import 1.4.0 queue-action --theme light --file ./selected-light
|
|
|
171
184
|
# Review facts, copy, translations, and selected images, then finalize.
|
|
172
185
|
releasekit validate 1.4.0
|
|
173
186
|
releasekit finalize 1.4.0
|
|
187
|
+
|
|
174
188
|
# Export when requested.
|
|
175
|
-
releasekit export --
|
|
189
|
+
releasekit export --out ./release-output
|
|
176
190
|
```
|
|
177
191
|
|
|
178
|
-
- Preparing creates only `release.yaml` with pinned Git boundaries. The agent reads commit history and relevant file diffs from Git as needed.
|
|
192
|
+
- Preparing creates only `release.yaml` with pinned Git boundaries. Its `sourceLocale` and `locales` always come from current project configuration. The agent reads commit history and relevant file diffs from Git as needed.
|
|
179
193
|
- Use `--from-root` for an explicitly requested full-history first release.
|
|
180
194
|
- `--to` defaults to the pinned SHA when preparing a saved baseline, and to `HEAD` otherwise; `--previous` can supply the comparison start.
|
|
181
195
|
- Edit existing drafts in place. `prepare` never overwrites them.
|
|
182
|
-
- Use `note add --no-image` for an
|
|
196
|
+
- Notes include images by default. Use `note add --no-image` only for an explicit text-only choice. Adding a note clears any previous `emptyReason`.
|
|
197
|
+
- 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`.
|
|
183
198
|
- Add `--json` for structured results or `--cwd` to select a project directory.
|
|
184
|
-
-
|
|
199
|
+
- 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).
|
|
200
|
+
- 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.
|
|
201
|
+
- 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`.
|
|
185
202
|
|
|
186
203
|
See the [agent workflow](kit/references/workflow.md) for ancestry rules and continuing existing releases.
|
|
187
204
|
|
|
@@ -198,6 +215,12 @@ Codex and Cursor share `.agents/skills` to avoid duplicate discovery. Claude Cod
|
|
|
198
215
|
|
|
199
216
|
## Image themes
|
|
200
217
|
|
|
218
|
+
Image work covers **every drafted note** by default, including grouped minor fixes and improvements. A group uses one visual brief and the configured image variants; individual bullets do not require separate images. 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.
|
|
219
|
+
|
|
220
|
+
**Minor Fixes and Minor Improvements reuse common originals across releases.** The image skill first checks `releasekit/common-images/minor-fixes/` or `releasekit/common-images/minor-improvements/`, importing reviewed compatible images and creating only missing originals or themes. Each release keeps its own copies, so changes to the common design do not rewrite earlier releases. Bullet edits and translations do not require new pictures. The directories are created when reviewed originals become available; see [common images](kit/references/common-images.md).
|
|
221
|
+
|
|
222
|
+
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.
|
|
223
|
+
|
|
201
224
|
**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.
|
|
202
225
|
|
|
203
226
|
Choose `scene.source` in each note's visual brief before planning:
|
|
@@ -217,6 +240,8 @@ visuals:
|
|
|
217
240
|
|
|
218
241
|
`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.
|
|
219
242
|
|
|
243
|
+
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).
|
|
244
|
+
|
|
220
245
|
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.
|
|
221
246
|
|
|
222
247
|
<details>
|
|
@@ -233,7 +258,7 @@ releasekit image import 1.4.0 product-detail --theme shared --file ./approved-ca
|
|
|
233
258
|
|
|
234
259
|
The CLI keeps the original bytes, dimensions, and colors. One `shared` asset serves both viewer themes without generating or duplicating another file.
|
|
235
260
|
|
|
236
|
-
If genuine dark/light captures exist, import them with `--theme dark` and `--theme light` instead.
|
|
261
|
+
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.
|
|
237
262
|
|
|
238
263
|
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.
|
|
239
264
|
|
|
@@ -268,6 +293,8 @@ releasekit/
|
|
|
268
293
|
└── assets/ # Selected raster images
|
|
269
294
|
```
|
|
270
295
|
|
|
296
|
+
Major capabilities and changes that warrant individual attention get standalone notes. Small user-visible corrections and conveniences are collected into separate **Minor Fixes** and **Minor Improvements** notes, each with a short bullet list, normally after the main notes in that release. Every bullet retains its supporting evidence and selected translations. See [note grouping](kit/references/writing.md#group-minor-changes).
|
|
297
|
+
|
|
271
298
|
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.
|
|
272
299
|
|
|
273
300
|
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.
|
|
@@ -289,12 +316,13 @@ Translations track source fingerprints, and finalized releases record content fi
|
|
|
289
316
|
| [Choosing generated or supplied media](kit/references/media-sources.md) | Source selection, pending captures, and shared assets. |
|
|
290
317
|
| [Composition recipes](kit/references/composition-recipes.md) | Eight presentation categories matched to the feature and its source. |
|
|
291
318
|
| [Theme pairs and cost](kit/references/theme-pairing.md) | Shared geometry, single-theme policies, and reuse. |
|
|
319
|
+
| [Common images](kit/references/common-images.md) | Reusing minor-group originals across releases, missing themes, and preserved release copies. |
|
|
292
320
|
| [File contract](kit/references/format.md) · [JSON schemas](schemas) | Authoring files and the public export format. |
|
|
293
321
|
| [Worked examples](examples/README.md) | Paired illustrations, supplied-image workflow, independent briefs, and three-release bundles. |
|
|
294
322
|
|
|
295
|
-
Visual guidance uses independent, brand-neutral descriptions. Each illustration should communicate the actual feature through its own scene. Worked examples demonstrate the process;
|
|
323
|
+
Visual guidance uses independent, brand-neutral descriptions. Each illustration should communicate the actual feature through its own scene. Worked examples demonstrate the process; standalone notes get their own composition, while minor groups reuse their common scene.
|
|
296
324
|
|
|
297
|
-
The built-in guidance covers source selection, a scene contract, semantic palette roles, theme-pair invariants, text rules, cost-aware reuse, and visual acceptance checks. Generated icons are compact flat monochrome glyphs. Physical details and content previews use supplied images rather than invented 3D objects or decorative scenes.
|
|
325
|
+
The built-in guidance covers source selection, a scene contract, semantic palette roles, theme-pair invariants, text rules, cost-aware reuse, and visual acceptance checks. Generated icons are compact flat neutral monochrome glyphs. Emphasis starts with composition and value contrast; the project accent is optional and reserved for a specific state, action, or information distinction that needs color. Entirely neutral images are complete outputs. Physical details and content previews use supplied images rather than invented 3D objects or decorative scenes.
|
|
298
326
|
|
|
299
327
|
## Development
|
|
300
328
|
|
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
|
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { Command, Option } from 'commander';
|
|
5
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';
|
|
@@ -50,6 +50,8 @@ note.command('add <version> <id>').description('Scaffold a note and its locale f
|
|
|
50
50
|
await addNote(project(), version, id, noteMetaSchema.shape.category.parse(options.category), options.image);
|
|
51
51
|
emit({ version, note: id, status: 'draft' });
|
|
52
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)));
|
|
53
55
|
const images = program.command('image').description('Plan themed illustrations and register selected files');
|
|
54
56
|
images.command('plan <version>').description('Plan generation or supplied-image requests without calling a model')
|
|
55
57
|
.option('--sync-config', 'apply current project image settings to this draft')
|
|
@@ -59,10 +61,11 @@ images.command('plan <version>').description('Plan generation or supplied-image
|
|
|
59
61
|
await syncImagePolicy(instance, version);
|
|
60
62
|
emit(await planImages(instance, version));
|
|
61
63
|
});
|
|
62
|
-
images.command('import <version> <note>').description('Import
|
|
63
|
-
.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']))
|
|
64
67
|
.requiredOption('--file <file>', 'selected local PNG, JPEG, or WebP')
|
|
65
|
-
.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 })));
|
|
66
69
|
const translation = program.command('translation').description('Track source freshness for reviewed translations');
|
|
67
70
|
translation.command('mark <version> <note>').description('Mark an already reviewed translation current')
|
|
68
71
|
.requiredOption('--locale <locale>', 'translation language code')
|
|
@@ -79,9 +82,9 @@ program.command('validate [version]').description('Validate one release or all r
|
|
|
79
82
|
program.command('finalize <version>').description('Validate and mark local release content ready')
|
|
80
83
|
.action(async (version) => emit(await finalize(project(), version)));
|
|
81
84
|
program.command('export').description('Export recent version groups and selected image variants')
|
|
82
|
-
.
|
|
83
|
-
.option('--limit <count>', 'number of version groups, including current', value => Number(value))
|
|
84
|
-
.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')
|
|
85
88
|
.requiredOption('--out <directory>', 'new output directory')
|
|
86
89
|
.action(async (options) => emit(await exportBundle(project(), options.current, { ...options, out: path.resolve(program.opts().cwd, options.out) })));
|
|
87
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
|
}>;
|
package/dist/export.js
CHANGED
|
@@ -7,38 +7,47 @@ import { exists, readNote, write } from './files.js';
|
|
|
7
7
|
import { readVisual } from './content.js';
|
|
8
8
|
export async function exportBundle(project, current, options) {
|
|
9
9
|
const config = await project.config();
|
|
10
|
-
const
|
|
11
|
-
const history = await project.history(
|
|
12
|
-
const
|
|
13
|
-
const copies = [];
|
|
10
|
+
const version = current ?? await project.latestVersion();
|
|
11
|
+
const history = await project.history(version, options.limit ?? config.history.limit);
|
|
12
|
+
const languages = options.locale === undefined ? history[0].locales : [locale.parse(options.locale)];
|
|
14
13
|
for (const release of history) {
|
|
15
14
|
if (release.status !== 'ready')
|
|
16
15
|
throw new Error(`Release ${release.version} is still a draft.`);
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
for (const language of languages) {
|
|
17
|
+
if (!release.locales.includes(language))
|
|
18
|
+
throw new Error(`Release ${release.version} has no ${language} locale.`);
|
|
19
|
+
}
|
|
19
20
|
const checked = await validate(project, release.version);
|
|
20
21
|
if (!checked.valid)
|
|
21
22
|
throw new Error(checked.errors.join('\n'));
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
}
|
|
24
|
+
const bundles = [];
|
|
25
|
+
const copies = new Map();
|
|
26
|
+
for (const language of languages) {
|
|
27
|
+
const bundle = { schemaVersion: 1, currentVersion: version, locale: language, releases: [] };
|
|
28
|
+
for (const release of history) {
|
|
29
|
+
const entry = { version: release.version, releasedAt: release.releasedAt, previous: release.previous, notes: [] };
|
|
30
|
+
for (const note of release.notes) {
|
|
31
|
+
const text = await readNote(await project.releaseFile(release.version, `notes/${note.id}/${language}.md`));
|
|
32
|
+
const exported = { id: note.id, category: note.category, title: text.title, bodyMarkdown: text.body, image: null };
|
|
33
|
+
if (note.image) {
|
|
34
|
+
const visual = await readVisual(project, release.version, note.id);
|
|
35
|
+
const variants = activeVariants(visual, release.visuals);
|
|
36
|
+
exported.image = { alt: text.alt, fallbackTheme: variants[0], variants: {} };
|
|
37
|
+
for (const variant of variants) {
|
|
38
|
+
const asset = visual.variants[variant];
|
|
39
|
+
const relative = `assets/${release.version}/${path.posix.basename(asset.file)}`;
|
|
40
|
+
exported.image.variants[variant] = { src: relative, width: asset.width, height: asset.height };
|
|
41
|
+
copies.set(relative, await project.releaseFile(release.version, asset.file));
|
|
42
|
+
}
|
|
35
43
|
}
|
|
44
|
+
entry.notes.push(exported);
|
|
36
45
|
}
|
|
37
|
-
|
|
46
|
+
bundle.releases.push(entry);
|
|
38
47
|
}
|
|
39
|
-
|
|
48
|
+
bundleSchema.parse(bundle);
|
|
49
|
+
bundles.push(bundle);
|
|
40
50
|
}
|
|
41
|
-
bundleSchema.parse(bundle);
|
|
42
51
|
const destination = path.resolve(options.out);
|
|
43
52
|
const contentRoot = await project.content('releases');
|
|
44
53
|
const relativeToContent = path.relative(contentRoot, destination);
|
|
@@ -50,9 +59,14 @@ export async function exportBundle(project, current, options) {
|
|
|
50
59
|
}
|
|
51
60
|
if (await exists(destination))
|
|
52
61
|
throw new Error('The export destination already exists. Choose a new output directory.');
|
|
53
|
-
// No filesystem output is created until every selected release has passed validation.
|
|
54
|
-
for (const
|
|
55
|
-
await write(path.join(destination,
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
// No filesystem output is created until every selected release and locale has passed validation.
|
|
63
|
+
for (const [relative, src] of copies)
|
|
64
|
+
await write(path.join(destination, relative), await fs.readFile(src));
|
|
65
|
+
const files = [];
|
|
66
|
+
for (const bundle of bundles) {
|
|
67
|
+
const file = path.join(destination, `release-notes.${bundle.locale}.json`);
|
|
68
|
+
await write(file, JSON.stringify(bundle, null, 2) + '\n');
|
|
69
|
+
files.push(file);
|
|
70
|
+
}
|
|
71
|
+
return { files, releases: history.length, assets: copies.size };
|
|
58
72
|
}
|
package/dist/images.d.ts
CHANGED
|
@@ -31,7 +31,10 @@ export declare function planImages(project: Project, version: string): Promise<{
|
|
|
31
31
|
providedRequests: number;
|
|
32
32
|
costNote: string;
|
|
33
33
|
}>;
|
|
34
|
-
export
|
|
34
|
+
export interface ImportImageOptions {
|
|
35
|
+
source?: 'generated' | 'provided';
|
|
36
|
+
}
|
|
37
|
+
export declare function importImage(project: Project, version: string, noteId: string, variant: AssetVariant, source: string, options?: ImportImageOptions): Promise<{
|
|
35
38
|
file: string;
|
|
36
39
|
sha256: string;
|
|
37
40
|
sceneHash: string;
|