@iodes/releasekit 0.1.1 → 0.1.3

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 CHANGED
@@ -53,29 +53,37 @@ 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. Then edit `releasekit/config.yaml` to set your product name, source language, locales, and visual settings. Setup prints the skill invocation hints for each tool.
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 from v1.3.0 to v1.4.0.
64
- Write Korean and English notes.
65
- AI: Created releasekit/releases/1.4.0/
66
- Pinned the Git range and collected change evidence
67
- Wrote release notes in ko-KR and en-US
68
-
69
- You: Add images for the notes.
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 for the images required by these notes.
73
+ Choose: Prepare images (recommended), Review copy, or Stop for now.
74
+
75
+ You: Prepare images.
70
76
  AI: ✓ Generated dark/light explanations from shared scene briefs
71
77
  ✓ Reviewed and imported both variants for each generated graphic
72
78
  The product-detail image is pending; I need an approved capture
73
- or photo. Translation work can continue while it is pending.
79
+ or photo. Next: provide that image, or review the completed copy
80
+ while it is pending.
74
81
 
75
82
  You: Use ./approved-capture.png for the product-detail note.
76
83
  AI: ✓ Reviewed and imported it once for both viewer themes
84
+ Next: use releasekit-finalize to check and confirm 1.4.0.
77
85
 
78
- You: Review and finalize 1.4.0, then export up to three releases
86
+ You: Use releasekit-finalize for 1.4.0, then export up to three releases
79
87
  in English to ./release-output.
80
88
  AI: ✓ Validated notes, translations, and images
81
89
  ✓ Marked release 1.4.0 ready
@@ -84,22 +92,54 @@ AI: ✓ Validated notes, translations, and images
84
92
  └── assets/ ← Theme variants and shared supplied images
85
93
  ```
86
94
 
95
+ 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
+
97
+ 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.
98
+
87
99
  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.
88
100
 
101
+ ## Adopting ReleaseKit later
102
+
103
+ 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.
104
+
105
+ | Earlier history | Result |
106
+ | --- | --- |
107
+ | **Product introduction** (recommended for an established product) | A concise overview of capabilities at the baseline, grounded in that snapshot without reconstructing every old commit. |
108
+ | **Analyze history** | Notes based on the repository's beginning through the baseline. |
109
+ | **Skip** | No earlier entry; start recording subsequent changes. |
110
+
111
+ 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.
112
+
113
+ For example, introduce the product at `v1.3.0`, then record changes from there:
114
+
115
+ ```sh
116
+ releasekit start --at v1.3.0 --past summary --baseline-version 1.3.0
117
+ releasekit prepare 1.3.0
118
+ # Ask the agent to write, review, and finalize the baseline introduction.
119
+ releasekit prepare 1.4.0 --previous 1.3.0 --to v1.4.0
120
+ ```
121
+
122
+ 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.
123
+
124
+ `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).
125
+
89
126
  ## Workflow
90
127
 
91
128
  ```text
92
- Git range → Release draft → Notes + images + translationsValidateExport
129
+ Git range → Draft source + translations ImagesFinalizeOptional export
93
130
  ```
94
131
 
95
132
  | Skill | Purpose |
96
133
  | --- | --- |
97
- | `releasekit-draft` | Create or revise notes from a Git range. |
98
- | `releasekit-image` | Plan, generate or request, review, and import images. |
99
- | `releasekit-translate` | Translate notes and track source freshness. |
100
- | `releasekit-review` | Review content, evidence, images, and release readiness. |
134
+ | `releasekit-draft` | Choose a first-use baseline, write and revise source notes and selected translations, or refresh translations alone. |
135
+ | `releasekit-image` | Plan, generate or request, review, and import required images. |
136
+ | `releasekit-finalize` | Review copy, evidence, translations, and images; validate and mark the release ready; export when requested. |
101
137
 
102
- The agent handles editorial work, media selection, and image generation where appropriate. The CLI handles files, evidence, validation, and export. Finalizing a release marks local content ready; committing, publishing, and displaying it remain separate steps.
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.
139
+
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 also 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
+
142
+ 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.
103
143
 
104
144
  <details>
105
145
  <summary><strong>Step-by-step CLI workflow</strong></summary>
@@ -108,9 +148,15 @@ Replace the sample version, Git refs, and note ID with your own. If release `1.3
108
148
 
109
149
  ```sh
110
150
  releasekit prepare 1.4.0 --from v1.3.0 --to v1.4.0
151
+ # Save selected languages in release.yaml before adding notes.
152
+ # This example uses sourceLocale: en-US and locales: [en-US, ko-KR].
111
153
  releasekit note add 1.4.0 queue-action
112
154
 
113
- # Fill the notes, evidence references, and visual brief.
155
+ # Write the source and selected translations, then attach evidence.
156
+ # Review each translation before marking it current.
157
+ releasekit translation mark 1.4.0 queue-action --locale ko-KR
158
+
159
+ # Complete the visual brief for image work.
114
160
  # Choose an archetype and set scene.source to generated for this example.
115
161
  releasekit image plan 1.4.0
116
162
 
@@ -122,15 +168,16 @@ releasekit image plan 1.4.0
122
168
  # Generate and review the matching light image, then import it.
123
169
  releasekit image import 1.4.0 queue-action --theme light --file ./selected-light.png
124
170
 
125
- # Review the completed translation before marking it current.
126
- releasekit translation mark 1.4.0 queue-action --locale en-US
171
+ # Review facts, copy, translations, and selected images, then finalize.
127
172
  releasekit validate 1.4.0
128
173
  releasekit finalize 1.4.0
129
- releasekit export --current 1.4.0 --limit 3 --locale ko-KR --out ./release-output
174
+ # Export when requested.
175
+ releasekit export --current 1.4.0 --limit 3 --locale en-US --out ./release-output
130
176
  ```
131
177
 
178
+ - Preparing creates only `release.yaml` with pinned Git boundaries. The agent reads commit history and relevant file diffs from Git as needed.
132
179
  - Use `--from-root` for an explicitly requested full-history first release.
133
- - `--to` defaults to `HEAD`; `--previous` can supply the comparison start.
180
+ - `--to` defaults to the pinned SHA when preparing a saved baseline, and to `HEAD` otherwise; `--previous` can supply the comparison start.
134
181
  - Edit existing drafts in place. `prepare` never overwrites them.
135
182
  - Use `note add --no-image` for an intentionally text-only note.
136
183
  - Add `--json` for structured results or `--cwd` to select a project directory.
@@ -215,14 +262,14 @@ releasekit/
215
262
  └── releases/
216
263
  └── 1.4.0/
217
264
  ├── release.yaml # Metadata, note order, previous release
218
- ├── evidence.json # Pinned Git evidence
219
- ├── changes.patch # Net change for this release
220
265
  ├── notes/ # Markdown for each locale
221
266
  ├── visuals/ # Scene briefs, media sources, and variants
222
267
  ├── prompts/ # Prompts for pending generated variants
223
268
  └── assets/ # Selected raster images
224
269
  ```
225
270
 
271
+ 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
+
226
273
  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.
227
274
 
228
275
  The bundle contains display data and relative assets. Git evidence, prompts, and private source paths stay out of the export. Consumers safely render `bodyMarkdown` and select `image.variants[theme]`, falling back to `image.variants[image.fallbackTheme]` when needed. Text-only notes have `image: null`.
@@ -235,7 +282,8 @@ Translations track source fingerprints, and finalized releases record content fi
235
282
 
236
283
  | Guide | What it covers |
237
284
  | --- | --- |
238
- | [Agent workflow](kit/references/workflow.md) | Git boundaries, drafts, review, and export. |
285
+ | [First use in an existing product](kit/references/adoption.md) | Starting points, product introductions, historical analysis, and skipped history. |
286
+ | [Agent workflow](kit/references/workflow.md) | Git boundaries, drafts and translations, images, finalization, and export. |
239
287
  | [Writing and translation](kit/references/writing.md) | Product copy, evidence, and locale freshness. |
240
288
  | [Visual language](kit/references/visual-language.md) | Composition, hierarchy, materials, and acceptance checks. |
241
289
  | [Choosing generated or supplied media](kit/references/media-sources.md) | Source selection, pending captures, and shared assets. |
@@ -260,7 +308,7 @@ npm run build
260
308
  npm pack --dry-run
261
309
  ```
262
310
 
263
- 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.
311
+ 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.
264
312
 
265
313
  <details>
266
314
  <summary><strong>Install from a local checkout</strong></summary>
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
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
7
  import { addNote, markTranslation, syncImagePolicy } from './content.js';
8
8
  import { planImages, importImage } from './images.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', 'HEAD')
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')
package/dist/git.d.ts CHANGED
@@ -1,10 +1,22 @@
1
- import { type Evidence, type Release } from './model.js';
1
+ import type { Release } from './model.js';
2
+ interface GitChanges {
3
+ source: Release['source'];
4
+ commits: Array<{
5
+ sha: string;
6
+ subject: string;
7
+ }>;
8
+ files: Array<{
9
+ status: string;
10
+ path: string;
11
+ oldPath?: string;
12
+ }>;
13
+ }
2
14
  export declare function git(cwd: string, args: string[], input?: string): string;
3
15
  export declare function repoRoot(cwd: string): string;
4
16
  export declare function resolveCommit(root: string, ref: string): string;
5
17
  export declare function isAncestor(root: string, base: string, head: string): boolean;
6
- export declare function collect(root: string, from: string | null, to: string): {
7
- evidence: Evidence;
8
- patch: string;
9
- };
10
- export declare function checkPrevious(root: string, previous: Release, evidence: Evidence): void;
18
+ export declare function resolveRange(root: string, from: string | null, to: string): Release['source'];
19
+ export declare function collect(root: string, from: string | null, to: string): GitChanges;
20
+ export declare function collectSnapshot(root: string, to: string): GitChanges;
21
+ export declare function checkPrevious(root: string, previous: Release, source: Release['source']): void;
22
+ export {};
package/dist/git.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { evidenceSchema } from './model.js';
3
2
  export function git(cwd, args, input) {
4
3
  const result = spawnSync('git', args, {
5
4
  cwd, input, encoding: 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024,
@@ -27,7 +26,7 @@ export function isAncestor(root, base, head) {
27
26
  return false;
28
27
  throw new Error(`Cannot establish Git ancestry: ${result.stderr || result.error?.message}`);
29
28
  }
30
- export function collect(root, from, to) {
29
+ export function resolveRange(root, from, to) {
31
30
  if (git(root, ['rev-parse', '--is-shallow-repository']).trim() === 'true') {
32
31
  throw new Error('Complete the shallow Git history before preparing a release; no fetch was performed.');
33
32
  }
@@ -38,6 +37,11 @@ export function collect(root, from, to) {
38
37
  if (fromSha && !isAncestor(root, fromSha, toSha)) {
39
38
  throw new Error('The start commit is not an ancestor of the end commit. Choose an explicit range on this release line.');
40
39
  }
40
+ return { fromRef: from, fromSha, toRef: to, toSha };
41
+ }
42
+ export function collect(root, from, to) {
43
+ const source = resolveRange(root, from, to);
44
+ const { fromSha, toSha } = source;
41
45
  const base = fromSha ?? git(root, ['hash-object', '-t', 'tree', '--stdin'], '').trim();
42
46
  const log = git(root, ['log', '--no-show-signature', '--format=%H%x00%s', fromSha ? `${fromSha}..${toSha}` : toSha, '--']);
43
47
  const commits = log.trimEnd() ? log.trimEnd().split('\n').map(line => {
@@ -54,14 +58,19 @@ export function collect(root, from, to) {
54
58
  else
55
59
  files.push({ status, path: first });
56
60
  }
57
- const patch = git(root, ['diff', '--no-ext-diff', '--no-textconv', '--find-renames', base, toSha, '--']);
61
+ return { source, commits, files };
62
+ }
63
+ export function collectSnapshot(root, to) {
64
+ const source = resolveRange(root, null, to);
65
+ const subject = git(root, ['show', '--no-show-signature', '--no-patch', '--format=%s', source.toSha, '--']).trimEnd();
66
+ const paths = git(root, ['ls-tree', '-r', '--name-only', '-z', source.toSha, '--']);
58
67
  return {
59
- evidence: evidenceSchema.parse({ schemaVersion: 1, source: { fromRef: from, fromSha, toRef: to, toSha }, commits, files }),
60
- patch,
68
+ source, commits: [{ sha: source.toSha, subject }],
69
+ files: paths.split('\0').filter(Boolean).map(path => ({ status: 'A', path })),
61
70
  };
62
71
  }
63
- export function checkPrevious(root, previous, evidence) {
64
- const boundary = evidence.source.fromSha;
72
+ export function checkPrevious(root, previous, source) {
73
+ const boundary = source.fromSha;
65
74
  if (!boundary || !isAncestor(root, previous.source.toSha, boundary)) {
66
75
  throw new Error('The previous release is not an ancestor of this comparison start. Choose the correct previous release.');
67
76
  }
package/dist/install.js CHANGED
@@ -6,7 +6,7 @@ import { Project } from './project.js';
6
6
  import { configSchema, defaultConfig } from './model.js';
7
7
  import { exists, within, write, writeYaml, digest } from './files.js';
8
8
  const resources = fileURLToPath(new URL('../kit/', import.meta.url));
9
- const names = ['releasekit-draft', 'releasekit-image', 'releasekit-translate', 'releasekit-review'];
9
+ const names = ['releasekit-draft', 'releasekit-image', 'releasekit-finalize'];
10
10
  const managedSchema = z.record(z.string(), z.string().regex(/^[a-f0-9]{64}$/));
11
11
  export async function installSkills(project) {
12
12
  const config = await project.config();
@@ -44,8 +44,8 @@ export async function installSkills(project) {
44
44
  }
45
45
  await write(marker, JSON.stringify(managed, null, 2) + '\n');
46
46
  return { tools: config.tools, written, conflicts, hints: {
47
- codex: 'Use $releasekit-draft, $releasekit-image, $releasekit-translate, or $releasekit-review.',
48
- claude: 'Use /releasekit-draft, /releasekit-image, /releasekit-translate, or /releasekit-review.',
47
+ codex: 'Use $releasekit-draft, $releasekit-image, or $releasekit-finalize.',
48
+ claude: 'Use /releasekit-draft, /releasekit-image, or /releasekit-finalize.',
49
49
  cursor: 'Use the installed releasekit-* skills from the agent skill picker or name them in your request.',
50
50
  } };
51
51
  }
package/dist/model.d.ts CHANGED
@@ -49,6 +49,23 @@ export declare const visualPolicySchema: z.ZodObject<{
49
49
  }, z.core.$strict>;
50
50
  }, z.core.$strict>;
51
51
  export type VisualPolicy = z.infer<typeof visualPolicySchema>;
52
+ export declare const historyStartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
53
+ ref: z.ZodString;
54
+ sha: z.ZodString;
55
+ past: z.ZodLiteral<"summary">;
56
+ version: z.ZodString;
57
+ }, z.core.$strict>, z.ZodObject<{
58
+ ref: z.ZodString;
59
+ sha: z.ZodString;
60
+ past: z.ZodLiteral<"history">;
61
+ version: z.ZodString;
62
+ }, z.core.$strict>, z.ZodObject<{
63
+ ref: z.ZodString;
64
+ sha: z.ZodString;
65
+ past: z.ZodLiteral<"skip">;
66
+ version: z.ZodNull;
67
+ }, z.core.$strict>], "past">;
68
+ export type HistoryStart = z.infer<typeof historyStartSchema>;
52
69
  export declare const configSchema: z.ZodObject<{
53
70
  schemaVersion: z.ZodLiteral<1>;
54
71
  product: z.ZodString;
@@ -56,6 +73,22 @@ export declare const configSchema: z.ZodObject<{
56
73
  locales: z.ZodArray<z.ZodString>;
57
74
  history: z.ZodObject<{
58
75
  limit: z.ZodNumber;
76
+ start: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
77
+ ref: z.ZodString;
78
+ sha: z.ZodString;
79
+ past: z.ZodLiteral<"summary">;
80
+ version: z.ZodString;
81
+ }, z.core.$strict>, z.ZodObject<{
82
+ ref: z.ZodString;
83
+ sha: z.ZodString;
84
+ past: z.ZodLiteral<"history">;
85
+ version: z.ZodString;
86
+ }, z.core.$strict>, z.ZodObject<{
87
+ ref: z.ZodString;
88
+ sha: z.ZodString;
89
+ past: z.ZodLiteral<"skip">;
90
+ version: z.ZodNull;
91
+ }, z.core.$strict>], "past">>;
59
92
  }, z.core.$strict>;
60
93
  visuals: z.ZodObject<{
61
94
  themes: z.ZodEnum<{
@@ -125,6 +158,10 @@ export declare const releaseSchema: z.ZodObject<{
125
158
  toRef: z.ZodString;
126
159
  toSha: z.ZodString;
127
160
  }, z.core.$strict>;
161
+ initialContent: z.ZodOptional<z.ZodEnum<{
162
+ history: "history";
163
+ summary: "summary";
164
+ }>>;
128
165
  sourceLocale: z.ZodString;
129
166
  locales: z.ZodArray<z.ZodString>;
130
167
  visuals: z.ZodObject<{
@@ -275,25 +312,6 @@ export declare const visualSchema: z.ZodObject<{
275
312
  }, z.core.$strict>;
276
313
  }, z.core.$strict>;
277
314
  export type Visual = z.infer<typeof visualSchema>;
278
- export declare const evidenceSchema: z.ZodObject<{
279
- schemaVersion: z.ZodLiteral<1>;
280
- source: z.ZodObject<{
281
- fromRef: z.ZodNullable<z.ZodString>;
282
- fromSha: z.ZodNullable<z.ZodString>;
283
- toRef: z.ZodString;
284
- toSha: z.ZodString;
285
- }, z.core.$strict>;
286
- commits: z.ZodArray<z.ZodObject<{
287
- sha: z.ZodString;
288
- subject: z.ZodString;
289
- }, z.core.$strict>>;
290
- files: z.ZodArray<z.ZodObject<{
291
- status: z.ZodString;
292
- path: z.ZodString;
293
- oldPath: z.ZodOptional<z.ZodString>;
294
- }, z.core.$strict>>;
295
- }, z.core.$strict>;
296
- export type Evidence = z.infer<typeof evidenceSchema>;
297
315
  export declare const bundleSchema: z.ZodObject<{
298
316
  schemaVersion: z.ZodLiteral<1>;
299
317
  currentVersion: z.ZodString;
package/dist/model.js CHANGED
@@ -17,10 +17,15 @@ export const visualPolicySchema = z.strictObject({
17
17
  dark: paletteSchema,
18
18
  light: paletteSchema,
19
19
  });
20
+ export const historyStartSchema = z.discriminatedUnion('past', [
21
+ z.strictObject({ ref: z.string().min(1), sha, past: z.literal('summary'), version: segment }),
22
+ z.strictObject({ ref: z.string().min(1), sha, past: z.literal('history'), version: segment }),
23
+ z.strictObject({ ref: z.string().min(1), sha, past: z.literal('skip'), version: z.null() }),
24
+ ]);
20
25
  export const configSchema = z.strictObject({
21
26
  schemaVersion: z.literal(1), product: z.string().min(1),
22
27
  sourceLocale: locale, locales: z.array(locale).min(1),
23
- history: z.strictObject({ limit: z.number().int().min(1).max(100) }),
28
+ history: z.strictObject({ limit: z.number().int().min(1).max(100), start: historyStartSchema.optional() }),
24
29
  visuals: visualPolicySchema,
25
30
  tools: z.array(z.enum(['codex', 'claude', 'cursor'])),
26
31
  });
@@ -35,7 +40,8 @@ export const noteMetaSchema = z.strictObject({
35
40
  export const releaseSchema = z.strictObject({
36
41
  schemaVersion: z.literal(1), version: segment, releasedAt: z.iso.date(),
37
42
  previous: segment.nullable(), status: z.enum(['draft', 'ready']),
38
- source: sourceSchema, sourceLocale: locale, locales: z.array(locale).min(1),
43
+ source: sourceSchema, initialContent: z.enum(['summary', 'history']).optional(),
44
+ sourceLocale: locale, locales: z.array(locale).min(1),
39
45
  visuals: visualPolicySchema, notes: z.array(noteMetaSchema),
40
46
  emptyReason: z.string().nullable(), contentHash: z.string().nullable(),
41
47
  });
@@ -64,11 +70,6 @@ export const visualSchema = z.strictObject({
64
70
  schemaVersion: z.literal(1), scene: sceneSchema,
65
71
  variants: z.strictObject({ dark: assetSchema.optional(), light: assetSchema.optional(), shared: assetSchema.optional() }),
66
72
  });
67
- export const evidenceSchema = z.strictObject({
68
- schemaVersion: z.literal(1), source: sourceSchema,
69
- commits: z.array(z.strictObject({ sha, subject: z.string() })),
70
- files: z.array(z.strictObject({ status: z.string(), path: z.string(), oldPath: z.string().optional() })),
71
- });
72
73
  const exportedImage = z.strictObject({
73
74
  src: z.string(), width: z.number().int().positive(), height: z.number().int().positive(),
74
75
  });
@@ -112,7 +113,7 @@ export function activeVariants(visual, policy) {
112
113
  }
113
114
  export function defaultConfig(product) {
114
115
  return {
115
- schemaVersion: 1, product, sourceLocale: 'ko-KR', locales: ['ko-KR', 'en-US'],
116
+ schemaVersion: 1, product, sourceLocale: 'en-US', locales: ['en-US'],
116
117
  history: { limit: 3 }, tools: ['codex', 'claude', 'cursor'],
117
118
  visuals: {
118
119
  themes: 'both', preset: 'quiet-product', width: 1280, height: 800, accent: '#4678ED',
package/dist/project.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ProjectConfig, type Release, type Evidence } from './model.js';
1
+ import { type HistoryStart, type ProjectConfig, type Release } from './model.js';
2
2
  export declare class Project {
3
3
  readonly root: string;
4
4
  constructor(root: string);
@@ -10,10 +10,15 @@ export declare class Project {
10
10
  release(version: string): Promise<Release>;
11
11
  save(release: Release): Promise<void>;
12
12
  versions(): Promise<string[]>;
13
- evidence(version: string): Promise<Evidence>;
14
13
  history(version: string, limit: number): Promise<Release[]>;
15
14
  }
16
15
  export declare function editable(release: Release): void;
16
+ export interface StartOptions {
17
+ at: string;
18
+ past: HistoryStart['past'];
19
+ version?: string;
20
+ }
21
+ export declare function startProject(project: Project, options: StartOptions): Promise<HistoryStart>;
17
22
  export interface PrepareOptions {
18
23
  from?: string;
19
24
  to?: string;
package/dist/project.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { configSchema, releaseSchema, evidenceSchema } from './model.js';
4
- import { exists, identifier, readYaml, within, writeYaml, write, KIT_DIR } from './files.js';
5
- import { repoRoot, collect, checkPrevious } from './git.js';
3
+ import { configSchema, historyStartSchema, releaseSchema } from './model.js';
4
+ import { exists, identifier, readYaml, within, writeYaml, KIT_DIR } from './files.js';
5
+ import { repoRoot, resolveCommit, resolveRange, checkPrevious } from './git.js';
6
6
  export class Project {
7
7
  root;
8
8
  constructor(root) { this.root = path.resolve(root); }
@@ -37,9 +37,6 @@ export class Project {
37
37
  const entries = await fs.readdir(folder, { withFileTypes: true });
38
38
  return entries.filter(e => e.isDirectory()).map(e => e.name).sort();
39
39
  }
40
- async evidence(version) {
41
- return evidenceSchema.parse(JSON.parse(await fs.readFile(await this.releaseFile(version, 'evidence.json'), 'utf8')));
42
- }
43
40
  async history(version, limit) {
44
41
  if (!Number.isInteger(limit) || limit < 1 || limit > 100)
45
42
  throw new Error('History limit must be an integer from 1 to 100.');
@@ -63,6 +60,24 @@ export function editable(release) {
63
60
  if (release.status !== 'draft')
64
61
  throw new Error('This release is ready. Set status to draft and contentHash to null before editing it.');
65
62
  }
63
+ export async function startProject(project, options) {
64
+ const config = await project.config();
65
+ if (config.history.start)
66
+ throw new Error('A history start is already configured. Reuse the saved choice; it was not overwritten.');
67
+ if ((await project.versions()).length)
68
+ throw new Error('History setup requires a project without releases. Continue an existing release line with --previous.');
69
+ if (options.past === 'skip' && options.version !== undefined)
70
+ throw new Error('--baseline-version is not used when --past is skip.');
71
+ if (options.past !== 'skip' && !options.version)
72
+ throw new Error('Specify --baseline-version for the baseline summary or history release.');
73
+ if (options.version)
74
+ identifier(options.version);
75
+ const source = resolveRange(project.root, null, options.at);
76
+ const start = historyStartSchema.parse({ ref: options.at, sha: source.toSha, past: options.past, version: options.version ?? null });
77
+ config.history.start = start;
78
+ await writeYaml(await project.content('config.yaml'), config);
79
+ return start;
80
+ }
66
81
  export async function prepare(project, version, options) {
67
82
  identifier(version);
68
83
  const directory = await project.releaseDir(version);
@@ -73,31 +88,62 @@ export async function prepare(project, version, options) {
73
88
  throw new Error('--from-root cannot be combined with --from or --previous.');
74
89
  if (options.firstRelease && options.previous)
75
90
  throw new Error('--first-release cannot be combined with --previous.');
91
+ const versions = await project.versions();
92
+ const start = config.history.start;
76
93
  let previous = options.previous ? await project.release(options.previous) : undefined;
77
- const from = options.fromRoot ? null : options.from ?? previous?.source.toSha;
94
+ let from = options.fromRoot ? null : options.from ?? previous?.source.toSha;
95
+ let to = options.to ?? 'HEAD';
96
+ let initialContent;
97
+ let savedStart = false;
98
+ if (start && start.past !== 'skip' && version === start.version) {
99
+ if (options.from !== undefined || options.previous !== undefined)
100
+ throw new Error('The configured baseline starts at the root and has no previous release.');
101
+ if (options.to !== undefined && resolveCommit(project.root, options.to) !== start.sha)
102
+ throw new Error('The requested end differs from the pinned history start.');
103
+ from = null;
104
+ to = start.sha;
105
+ initialContent = start.past;
106
+ }
107
+ else if (start && from === undefined && !options.firstRelease) {
108
+ if (start.past === 'skip' && !versions.length) {
109
+ from = start.sha;
110
+ savedStart = true;
111
+ }
112
+ else if (start.past !== 'skip' && versions.every(v => v === start.version)) {
113
+ if (!versions.includes(start.version))
114
+ throw new Error(`Prepare the configured baseline ${start.version} first, then continue with --previous ${start.version}.`);
115
+ previous = await project.release(start.version);
116
+ if (previous.source.toSha !== start.sha || previous.initialContent !== start.past)
117
+ throw new Error('The baseline release differs from the saved history start. Choose an explicit --previous release.');
118
+ from = previous.source.toSha;
119
+ }
120
+ }
78
121
  if (from === undefined)
79
- throw new Error('Specify --from, --previous, or --from-root.');
80
- const { evidence, patch } = collect(project.root, from, options.to ?? 'HEAD');
81
- if (!previous && !options.fromRoot && !options.firstRelease) {
82
- const existing = await Promise.all((await project.versions()).map(v => project.release(v)));
83
- const candidates = existing.filter(r => r.source.toSha === evidence.source.fromSha);
122
+ throw new Error('Specify --from, --previous, or --from-root, or configure a first-use boundary with start.');
123
+ const source = resolveRange(project.root, from, to);
124
+ if (initialContent && start)
125
+ source.toRef = start.ref;
126
+ if (savedStart && start)
127
+ source.fromRef = start.ref;
128
+ if (!previous && !options.fromRoot && !options.firstRelease && !initialContent) {
129
+ const existing = await Promise.all(versions.map(v => project.release(v)));
130
+ const candidates = existing.filter(r => r.source.toSha === source.fromSha);
84
131
  if (candidates.length === 1)
85
132
  previous = candidates[0];
86
133
  else if (existing.length)
87
134
  throw new Error('Previous release is ambiguous. Specify --previous, or --first-release for an independent release line.');
88
135
  }
89
136
  if (previous) {
90
- checkPrevious(project.root, previous, evidence);
137
+ checkPrevious(project.root, previous, source);
91
138
  await project.history(previous.version, 1);
92
139
  }
93
140
  const release = releaseSchema.parse({
94
141
  schemaVersion: 1, version, releasedAt: options.date ?? new Date().toISOString().slice(0, 10),
95
- previous: previous?.version ?? null, status: 'draft', source: evidence.source,
142
+ previous: previous?.version ?? null, status: 'draft', source,
143
+ ...(initialContent ? { initialContent } : {}),
96
144
  sourceLocale: config.sourceLocale, locales: config.locales, visuals: config.visuals,
97
145
  notes: [], emptyReason: null, contentHash: null,
98
146
  });
99
147
  await project.save(release);
100
- await write(await project.releaseFile(version, 'evidence.json'), JSON.stringify(evidence, null, 2) + '\n');
101
- await write(await project.releaseFile(version, 'changes.patch'), patch);
102
148
  return release;
103
149
  }
@@ -1,10 +1,17 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { z } from 'zod';
4
- import { configSchema, releaseSchema, visualSchema, noteTextSchema, evidenceSchema, bundleSchema } from './model.js';
4
+ import { configSchema, releaseSchema, visualSchema, noteTextSchema, bundleSchema } from './model.js';
5
5
  const directory = fileURLToPath(new URL('../schemas/', import.meta.url));
6
6
  await fs.mkdir(directory, { recursive: true });
7
- for (const [name, schema] of Object.entries({ config: configSchema, release: releaseSchema, visual: visualSchema, note: noteTextSchema, evidence: evidenceSchema, bundle: bundleSchema })) {
7
+ const schemas = { config: configSchema, release: releaseSchema, visual: visualSchema, note: noteTextSchema, bundle: bundleSchema };
8
+ for (const [name, schema] of Object.entries(schemas)) {
8
9
  const json = { ...z.toJSONSchema(schema), $id: `urn:releasekit:${name}:1` };
9
10
  await fs.writeFile(`${directory}/${name}.schema.json`, JSON.stringify(json, null, 2) + '\n');
10
11
  }
12
+ // Keep the generated directory aligned with the schemas the package exports.
13
+ const outputs = new Set(Object.keys(schemas).map(name => `${name}.schema.json`));
14
+ for (const file of await fs.readdir(directory)) {
15
+ if (file.endsWith('.schema.json') && !outputs.has(file))
16
+ await fs.unlink(`${directory}/${file}`);
17
+ }