@iodes/releasekit 0.1.7 → 0.2.1

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/dist/install.d.ts CHANGED
@@ -1,23 +1,56 @@
1
1
  import { Project } from './project.js';
2
2
  import { type ProjectConfig } from './model.js';
3
- export declare function installSkills(project: Project): Promise<{
3
+ export declare function installSkills(project: Project, tools?: ProjectConfig['tools']): Promise<{
4
4
  tools: ("claude" | "codex" | "cursor")[];
5
5
  written: string[];
6
6
  conflicts: string[];
7
+ unchanged: string[];
7
8
  hints: {
8
9
  codex: string;
9
10
  claude: string;
10
11
  cursor: string;
11
12
  };
12
13
  }>;
13
- export declare function initProject(project: Project, options: {
14
+ export declare function updateProject(project: Project): Promise<{
15
+ product: string;
16
+ migrations: string[];
17
+ backup: null;
18
+ tools: ("claude" | "codex" | "cursor")[];
19
+ written: string[];
20
+ conflicts: string[];
21
+ unchanged: string[];
22
+ hints: {
23
+ codex: string;
24
+ claude: string;
25
+ cursor: string;
26
+ };
27
+ } | {
28
+ product: string;
29
+ migrations: string[];
30
+ backup: string;
31
+ tools: ("claude" | "codex" | "cursor")[];
32
+ written: string[];
33
+ conflicts: string[];
34
+ unchanged: string[];
35
+ hints: {
36
+ codex: string;
37
+ claude: string;
38
+ cursor: string;
39
+ };
40
+ }>;
41
+ export declare function formatUpdate(result: Awaited<ReturnType<typeof updateProject>>): string;
42
+ export interface InitOptions {
14
43
  product?: string;
15
44
  tools?: ProjectConfig['tools'];
16
45
  themes?: ProjectConfig['visuals']['themes'];
17
- }): Promise<{
46
+ sourceLocale?: string;
47
+ locales?: string[];
48
+ }
49
+ export declare function initProject(project: Project, options: InitOptions): Promise<{
18
50
  tools: ("claude" | "codex" | "cursor")[];
19
51
  written: string[];
20
52
  conflicts: string[];
53
+ unchanged: string[];
21
54
  hints: {
22
55
  codex: string;
23
56
  claude: string;
package/dist/install.js CHANGED
@@ -3,18 +3,20 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { z } from 'zod';
5
5
  import { Project } from './project.js';
6
+ import { migrateConfig } from './migrate-config.js';
6
7
  import { configSchema, defaultConfig } from './model.js';
7
8
  import { exists, within, write, writeYaml, digest } from './files.js';
8
9
  const resources = fileURLToPath(new URL('../kit/', import.meta.url));
9
10
  const names = ['releasekit-draft', 'releasekit-image', 'releasekit-finalize'];
10
11
  const managedSchema = z.record(z.string(), z.string().regex(/^[a-f0-9]{64}$/));
11
- export async function installSkills(project) {
12
+ export async function installSkills(project, tools) {
12
13
  const config = await project.config();
14
+ const selected = tools ?? config.tools;
13
15
  const marker = await project.content('managed-skills.json');
14
16
  const managed = await exists(marker) ? managedSchema.parse(JSON.parse(await fs.readFile(marker, 'utf8'))) : {};
15
- const roots = new Set(config.tools.map(tool => tool === 'claude' ? '.claude/skills' : '.agents/skills'));
17
+ const roots = new Set(selected.map(tool => tool === 'claude' ? '.claude/skills' : '.agents/skills'));
16
18
  const references = (await fs.readdir(path.join(resources, 'references'))).filter(file => file.endsWith('.md')).sort();
17
- const written = [], conflicts = [];
19
+ const written = [], conflicts = [], unchanged = [];
18
20
  for (const root of roots) {
19
21
  for (const name of names) {
20
22
  const sourceFiles = [
@@ -29,6 +31,7 @@ export async function installSkills(project) {
29
31
  const actual = digest(await fs.readFile(destination));
30
32
  if (actual === expected) {
31
33
  managed[item.destination] = expected;
34
+ unchanged.push(item.destination);
32
35
  continue;
33
36
  }
34
37
  if (actual !== managed[item.destination]) {
@@ -43,12 +46,30 @@ export async function installSkills(project) {
43
46
  }
44
47
  }
45
48
  await write(marker, JSON.stringify(managed, null, 2) + '\n');
46
- return { tools: config.tools, written, conflicts, hints: {
49
+ return { tools: selected, written, conflicts, unchanged, hints: {
47
50
  codex: 'Use $releasekit-draft, $releasekit-image, or $releasekit-finalize.',
48
51
  claude: 'Use /releasekit-draft, /releasekit-image, or /releasekit-finalize.',
49
52
  cursor: 'Use the installed releasekit-* skills from the agent skill picker or name them in your request.',
50
53
  } };
51
54
  }
55
+ export async function updateProject(project) {
56
+ const migration = await migrateConfig(project);
57
+ return { ...await installSkills(project, ['codex', 'claude', 'cursor']), ...migration };
58
+ }
59
+ export function formatUpdate(result) {
60
+ const lines = [result.conflicts.length ? 'Update needs attention.' : result.tools.length ? 'Skills are up to date.' : 'No agent tools selected.',
61
+ ` Updated: ${result.written.length} files`, ` Already current: ${result.unchanged.length} files`, ` Modified files preserved: ${result.conflicts.length}`];
62
+ if (result.migrations.length) {
63
+ lines.push('', ...result.migrations.map(item => ` Configuration: ${item}`), ` Original configuration: ${result.backup}`);
64
+ }
65
+ if (result.conflicts.length) {
66
+ lines.push('', ...result.conflicts.map(file => ` ! ${file}`), 'Compare these files with the installed package templates and merge the changes you want to keep.');
67
+ }
68
+ if (result.tools.length)
69
+ lines.push('', ...result.tools.map(tool => ` ${tool}: ${result.hints[tool]}`));
70
+ lines.push('', 'Next: releasekit status');
71
+ return lines.join('\n');
72
+ }
52
73
  export async function initProject(project, options) {
53
74
  const file = await project.content('config.yaml');
54
75
  if (await exists(file))
@@ -58,6 +79,13 @@ export async function initProject(project, options) {
58
79
  config.tools = [...new Set(options.tools)];
59
80
  if (options.themes)
60
81
  config.visuals.themes = options.themes;
61
- await writeYaml(file, configSchema.parse(config));
82
+ if (options.sourceLocale !== undefined)
83
+ config.sourceLocale = options.sourceLocale;
84
+ config.locales = options.locales ?? [config.sourceLocale];
85
+ configSchema.parse(config);
86
+ if (!config.locales.includes(config.sourceLocale) || new Set(config.locales).size !== config.locales.length) {
87
+ throw new Error('Project locales must be unique and include the source locale.');
88
+ }
89
+ await writeYaml(file, config);
62
90
  return { config: file, ...await installSkills(project) };
63
91
  }
@@ -0,0 +1,10 @@
1
+ import { Project } from './project.js';
2
+ export declare function migrateConfig(project: Project): Promise<{
3
+ product: string;
4
+ migrations: string[];
5
+ backup: null;
6
+ } | {
7
+ product: string;
8
+ migrations: string[];
9
+ backup: string;
10
+ }>;
@@ -0,0 +1,44 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { parseDocument } from 'yaml';
3
+ import { Project, parseProjectConfig } from './project.js';
4
+ import { canonical, exists, parseYaml, write } from './files.js';
5
+ export async function migrateConfig(project) {
6
+ const file = await project.content('config.yaml');
7
+ if (!(await exists(file)))
8
+ throw new Error('ReleaseKit is not initialized. Run releasekit init first.');
9
+ const original = await fs.readFile(file, 'utf8');
10
+ const raw = parseYaml(original);
11
+ const migrations = [];
12
+ if (raw && typeof raw === 'object' && 'history' in raw && raw.history && typeof raw.history === 'object' && 'limit' in raw.history) {
13
+ delete raw.history.limit;
14
+ migrations.push('Removed history.limit; use export --limit to limit exported releases. Omit --limit to export all releases.');
15
+ }
16
+ // Validate the complete proposed configuration before touching any files.
17
+ const config = parseProjectConfig(raw);
18
+ if (!migrations.length)
19
+ return { product: config.product, migrations, backup: null };
20
+ const document = parseDocument(original);
21
+ document.deleteIn(['history', 'limit']);
22
+ let migrated = document.toString();
23
+ if (original.includes('\r\n'))
24
+ migrated = migrated.replace(/\r?\n/g, '\r\n');
25
+ if (original.startsWith('\uFEFF') && !migrated.startsWith('\uFEFF'))
26
+ migrated = '\uFEFF' + migrated;
27
+ if (canonical(parseYaml(migrated)) !== canonical(raw))
28
+ throw new Error('Configuration migration would change unrelated settings. No files were changed.');
29
+ const backup = await project.content('config.yaml.before-update');
30
+ try {
31
+ await fs.writeFile(backup, original, { flag: 'wx' });
32
+ }
33
+ catch (error) {
34
+ if (error.code !== 'EEXIST')
35
+ throw error;
36
+ if (await fs.readFile(backup, 'utf8') !== original) {
37
+ throw new Error(`A different configuration backup already exists at ${backup}. Move it aside before running update again; no files were overwritten.`);
38
+ }
39
+ }
40
+ if (await fs.readFile(file, 'utf8') !== original)
41
+ throw new Error('Configuration changed during update. Run update again; the configuration was not overwritten.');
42
+ await write(file, migrated);
43
+ return { product: config.product, migrations, backup };
44
+ }
package/dist/model.d.ts CHANGED
@@ -1,5 +1,16 @@
1
1
  import { z } from 'zod';
2
2
  export declare const segment: z.ZodString;
3
+ export declare const channelName: z.ZodString;
4
+ export declare const channelRefSchema: z.ZodObject<{
5
+ channel: z.ZodString;
6
+ version: z.ZodString;
7
+ }, z.core.$strict>;
8
+ export type ReleaseRef = {
9
+ version: string;
10
+ channel?: string;
11
+ };
12
+ export type ReleaseId = string | ReleaseRef;
13
+ export declare const releasedAtSchema: z.ZodUnion<readonly [z.ZodISODate, z.ZodISODateTime]>;
3
14
  export declare const sha: z.ZodString;
4
15
  export declare const locale: z.ZodString;
5
16
  export declare const theme: z.ZodEnum<{
@@ -66,13 +77,33 @@ export declare const historyStartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
66
77
  version: z.ZodNull;
67
78
  }, z.core.$strict>], "past">;
68
79
  export type HistoryStart = z.infer<typeof historyStartSchema>;
80
+ export declare const channelsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
81
+ include: z.ZodArray<z.ZodString>;
82
+ history: z.ZodOptional<z.ZodObject<{
83
+ start: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
84
+ ref: z.ZodString;
85
+ sha: z.ZodString;
86
+ past: z.ZodLiteral<"summary">;
87
+ version: z.ZodString;
88
+ }, z.core.$strict>, z.ZodObject<{
89
+ ref: z.ZodString;
90
+ sha: z.ZodString;
91
+ past: z.ZodLiteral<"history">;
92
+ version: z.ZodString;
93
+ }, z.core.$strict>, z.ZodObject<{
94
+ ref: z.ZodString;
95
+ sha: z.ZodString;
96
+ past: z.ZodLiteral<"skip">;
97
+ version: z.ZodNull;
98
+ }, z.core.$strict>], "past">>;
99
+ }, z.core.$strict>>;
100
+ }, z.core.$strict>>;
69
101
  export declare const configSchema: z.ZodObject<{
70
102
  schemaVersion: z.ZodLiteral<1>;
71
103
  product: z.ZodString;
72
104
  sourceLocale: z.ZodString;
73
105
  locales: z.ZodArray<z.ZodString>;
74
- history: z.ZodObject<{
75
- limit: z.ZodNumber;
106
+ history: z.ZodOptional<z.ZodObject<{
76
107
  start: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
77
108
  ref: z.ZodString;
78
109
  sha: z.ZodString;
@@ -89,7 +120,28 @@ export declare const configSchema: z.ZodObject<{
89
120
  past: z.ZodLiteral<"skip">;
90
121
  version: z.ZodNull;
91
122
  }, z.core.$strict>], "past">>;
92
- }, z.core.$strict>;
123
+ }, z.core.$strict>>;
124
+ channels: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodRecord<z.ZodString, z.ZodObject<{
125
+ include: z.ZodArray<z.ZodString>;
126
+ history: z.ZodOptional<z.ZodObject<{
127
+ start: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
128
+ ref: z.ZodString;
129
+ sha: z.ZodString;
130
+ past: z.ZodLiteral<"summary">;
131
+ version: z.ZodString;
132
+ }, z.core.$strict>, z.ZodObject<{
133
+ ref: z.ZodString;
134
+ sha: z.ZodString;
135
+ past: z.ZodLiteral<"history">;
136
+ version: z.ZodString;
137
+ }, z.core.$strict>, z.ZodObject<{
138
+ ref: z.ZodString;
139
+ sha: z.ZodString;
140
+ past: z.ZodLiteral<"skip">;
141
+ version: z.ZodNull;
142
+ }, z.core.$strict>], "past">>;
143
+ }, z.core.$strict>>;
144
+ }, z.core.$strict>>]>>;
93
145
  visuals: z.ZodObject<{
94
146
  themes: z.ZodEnum<{
95
147
  both: "both";
@@ -146,8 +198,12 @@ export type NoteMeta = z.infer<typeof noteMetaSchema>;
146
198
  export declare const releaseSchema: z.ZodObject<{
147
199
  schemaVersion: z.ZodLiteral<1>;
148
200
  version: z.ZodString;
149
- releasedAt: z.ZodISODate;
150
- previous: z.ZodNullable<z.ZodString>;
201
+ channel: z.ZodOptional<z.ZodString>;
202
+ releasedAt: z.ZodUnion<readonly [z.ZodISODate, z.ZodISODateTime]>;
203
+ previous: z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
204
+ channel: z.ZodString;
205
+ version: z.ZodString;
206
+ }, z.core.$strict>]>>;
151
207
  status: z.ZodEnum<{
152
208
  draft: "draft";
153
209
  ready: "ready";
@@ -316,10 +372,16 @@ export declare const bundleSchema: z.ZodObject<{
316
372
  schemaVersion: z.ZodLiteral<1>;
317
373
  currentVersion: z.ZodString;
318
374
  locale: z.ZodString;
375
+ viewChannel: z.ZodOptional<z.ZodString>;
376
+ currentChannel: z.ZodOptional<z.ZodString>;
319
377
  releases: z.ZodArray<z.ZodObject<{
320
378
  version: z.ZodString;
321
- releasedAt: z.ZodISODate;
322
- previous: z.ZodNullable<z.ZodString>;
379
+ channel: z.ZodOptional<z.ZodString>;
380
+ releasedAt: z.ZodUnion<readonly [z.ZodISODate, z.ZodISODateTime]>;
381
+ previous: z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
382
+ channel: z.ZodString;
383
+ version: z.ZodString;
384
+ }, z.core.$strict>]>>;
323
385
  notes: z.ZodArray<z.ZodObject<{
324
386
  id: z.ZodString;
325
387
  category: z.ZodEnum<{
package/dist/model.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  export const segment = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,95}$/);
3
+ export const channelName = z.string().regex(/^(?!(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$)[a-z][a-z0-9-]{0,62}$/);
4
+ export const channelRefSchema = z.strictObject({ channel: channelName, version: segment });
5
+ export const releasedAtSchema = z.union([z.iso.date(), z.iso.datetime({ offset: true }).regex(/T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/)]);
3
6
  export const sha = z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/);
4
7
  export const locale = z.string().regex(/^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/);
5
8
  export const theme = z.enum(['dark', 'light']);
@@ -27,10 +30,25 @@ export const historyStartSchema = z.discriminatedUnion('past', [
27
30
  z.strictObject({ ref: z.string().min(1), sha, past: z.literal('history'), version: segment }),
28
31
  z.strictObject({ ref: z.string().min(1), sha, past: z.literal('skip'), version: z.null() }),
29
32
  ]);
33
+ const historySettings = z.strictObject({ start: historyStartSchema.optional() });
34
+ export const channelsSchema = z.record(channelName, z.strictObject({
35
+ include: z.array(channelName).min(1), history: historySettings.optional(),
36
+ })).superRefine((channels, ctx) => {
37
+ if (!Object.keys(channels).length)
38
+ ctx.addIssue({ code: 'custom', message: 'Use channels: false to disable channels.' });
39
+ for (const [name, settings] of Object.entries(channels)) {
40
+ if (new Set(settings.include).size !== settings.include.length)
41
+ ctx.addIssue({ code: 'custom', path: [name, 'include'], message: 'Included channels must be unique.' });
42
+ for (const included of settings.include)
43
+ if (!Object.hasOwn(channels, included))
44
+ ctx.addIssue({ code: 'custom', path: [name, 'include'], message: `Unknown included channel: ${included}` });
45
+ }
46
+ });
30
47
  export const configSchema = z.strictObject({
31
48
  schemaVersion: z.literal(1), product: z.string().min(1),
32
49
  sourceLocale: locale, locales: z.array(locale).min(1),
33
- history: z.strictObject({ limit: z.number().int().min(1).max(100), start: historyStartSchema.optional() }),
50
+ history: historySettings.optional(),
51
+ channels: z.union([z.literal(false), channelsSchema]).optional(),
34
52
  visuals: visualPolicySchema,
35
53
  tools: z.array(z.enum(['codex', 'claude', 'cursor'])),
36
54
  });
@@ -43,12 +61,16 @@ export const noteMetaSchema = z.strictObject({
43
61
  commits: z.array(sha), paths: z.array(z.string()), image: z.boolean(),
44
62
  });
45
63
  export const releaseSchema = z.strictObject({
46
- schemaVersion: z.literal(1), version: segment, releasedAt: z.iso.date(),
47
- previous: segment.nullable(), status: z.enum(['draft', 'ready']),
64
+ schemaVersion: z.literal(1), version: segment, channel: channelName.optional(), releasedAt: releasedAtSchema,
65
+ previous: z.union([segment, channelRefSchema]).nullable(), status: z.enum(['draft', 'ready']),
48
66
  source: sourceSchema, initialContent: z.enum(['summary', 'history']).optional(),
49
67
  sourceLocale: locale, locales: z.array(locale).min(1),
50
68
  visuals: visualPolicySchema, notes: z.array(noteMetaSchema),
51
69
  emptyReason: z.string().nullable(), contentHash: z.string().nullable(),
70
+ }).superRefine((release, ctx) => {
71
+ if (release.previous !== null && (release.channel === undefined ? typeof release.previous !== 'string' : typeof release.previous === 'string')) {
72
+ ctx.addIssue({ code: 'custom', path: ['previous'], message: 'Channel releases require a channel/version predecessor; unchanneled releases require a version string.' });
73
+ }
52
74
  });
53
75
  export const noteTextSchema = z.strictObject({
54
76
  title: z.string().min(1), alt: z.string(), sourceHash: z.string().nullable(),
@@ -80,8 +102,9 @@ const exportedImage = z.strictObject({
80
102
  });
81
103
  export const bundleSchema = z.strictObject({
82
104
  schemaVersion: z.literal(1), currentVersion: segment, locale,
105
+ viewChannel: channelName.optional(), currentChannel: channelName.optional(),
83
106
  releases: z.array(z.strictObject({
84
- version: segment, releasedAt: z.iso.date(), previous: segment.nullable(),
107
+ version: segment, channel: channelName.optional(), releasedAt: releasedAtSchema, previous: z.union([segment, channelRefSchema]).nullable(),
85
108
  notes: z.array(z.strictObject({
86
109
  id: segment, category: noteMetaSchema.shape.category, title: z.string(), bodyMarkdown: z.string(),
87
110
  image: z.strictObject({
@@ -119,7 +142,7 @@ export function activeVariants(visual, policy) {
119
142
  export function defaultConfig(product) {
120
143
  return {
121
144
  schemaVersion: 1, product, sourceLocale: 'en-US', locales: ['en-US'],
122
- history: { limit: 3 }, tools: ['codex', 'claude', 'cursor'],
145
+ tools: ['codex', 'claude', 'cursor'],
123
146
  visuals: {
124
147
  themes: 'both', preset: 'quiet-product', width: 1280, height: 800, accent: '#4678ED',
125
148
  dark: { canvas: '#242527', surface: '#18191B', raised: '#343638', primary: '#B9BBBE', secondary: '#777B80', divider: '#46494D' },
package/dist/move.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { Project } from './project.js';
2
+ import { type ReleaseRef } from './model.js';
3
+ export interface MoveOptions {
4
+ fromChannel?: string;
5
+ toChannel?: string;
6
+ toUnchanneled?: boolean;
7
+ after?: string;
8
+ atStart?: boolean;
9
+ dryRun?: boolean;
10
+ }
11
+ export declare function moveReleases(project: Project, versions: string[], options: MoveOptions): Promise<{
12
+ dryRun: boolean;
13
+ moved: {
14
+ from: ReleaseRef;
15
+ to: ReleaseRef;
16
+ status: "draft" | "ready";
17
+ }[];
18
+ updatedReleases: ReleaseRef[];
19
+ updatedFiles: string[];
20
+ paths: {
21
+ from: string;
22
+ to: string;
23
+ }[];
24
+ }>;