@regardio/dev 1.24.0 → 2.0.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.
Files changed (128) hide show
  1. package/README.md +2 -2
  2. package/dist/bin/ship/hotfix.bin.mjs +140 -0
  3. package/dist/bin/ship/production.bin.mjs +120 -0
  4. package/dist/bin/ship/staging.bin.mjs +70 -0
  5. package/dist/bin/ship/utils-BQ-JZ2D5.mjs +45 -0
  6. package/dist/playwright/index.d.mts +24 -0
  7. package/dist/playwright/index.mjs +61 -0
  8. package/dist/vitest/node.d.mts +22 -0
  9. package/dist/vitest/node.mjs +28 -0
  10. package/dist/vitest/react.d.mts +22 -0
  11. package/dist/vitest/react.mjs +28 -0
  12. package/docs/en/README.md +95 -0
  13. package/docs/en/agents.md +57 -0
  14. package/docs/en/standards/api.md +324 -0
  15. package/docs/en/standards/coding.md +144 -0
  16. package/docs/en/standards/commits.md +111 -0
  17. package/docs/en/standards/documentation.md +173 -0
  18. package/docs/en/standards/naming.md +180 -0
  19. package/docs/en/standards/principles.md +84 -0
  20. package/docs/en/standards/react.md +246 -0
  21. package/docs/en/standards/sql.md +258 -0
  22. package/docs/en/standards/testing.md +139 -0
  23. package/docs/en/standards/writing.md +119 -0
  24. package/docs/en/tools/biome.md +89 -0
  25. package/docs/en/tools/commitlint.md +92 -0
  26. package/docs/en/tools/dependencies.md +116 -0
  27. package/docs/en/tools/husky.md +90 -0
  28. package/docs/en/tools/markdownlint.md +84 -0
  29. package/docs/en/tools/playwright.md +117 -0
  30. package/docs/en/tools/releases.md +242 -0
  31. package/docs/en/tools/typescript.md +89 -0
  32. package/docs/en/tools/vitest.md +146 -0
  33. package/package.json +58 -70
  34. package/src/biome/preset.json +3 -0
  35. package/templates/changeset/README.md +14 -0
  36. package/templates/changeset/config.json +11 -0
  37. package/templates/github/release.yml +77 -0
  38. package/dist/bin/exec/clean.d.ts +0 -3
  39. package/dist/bin/exec/clean.d.ts.map +0 -1
  40. package/dist/bin/exec/clean.js +0 -25
  41. package/dist/bin/exec/clean.test.d.ts +0 -2
  42. package/dist/bin/exec/clean.test.d.ts.map +0 -1
  43. package/dist/bin/exec/clean.test.js +0 -45
  44. package/dist/bin/exec/husky.d.ts +0 -3
  45. package/dist/bin/exec/husky.d.ts.map +0 -1
  46. package/dist/bin/exec/husky.js +0 -9
  47. package/dist/bin/exec/p.d.ts +0 -3
  48. package/dist/bin/exec/p.d.ts.map +0 -1
  49. package/dist/bin/exec/p.js +0 -8
  50. package/dist/bin/exec/s.d.ts +0 -3
  51. package/dist/bin/exec/s.d.ts.map +0 -1
  52. package/dist/bin/exec/s.js +0 -8
  53. package/dist/bin/exec/tsc.d.ts +0 -3
  54. package/dist/bin/exec/tsc.d.ts.map +0 -1
  55. package/dist/bin/exec/tsc.js +0 -8
  56. package/dist/bin/lint/biome.d.ts +0 -3
  57. package/dist/bin/lint/biome.d.ts.map +0 -1
  58. package/dist/bin/lint/biome.js +0 -8
  59. package/dist/bin/lint/commit.d.ts +0 -3
  60. package/dist/bin/lint/commit.d.ts.map +0 -1
  61. package/dist/bin/lint/commit.js +0 -8
  62. package/dist/bin/lint/md.d.ts +0 -3
  63. package/dist/bin/lint/md.d.ts.map +0 -1
  64. package/dist/bin/lint/md.js +0 -16
  65. package/dist/bin/lint/package.d.ts +0 -4
  66. package/dist/bin/lint/package.d.ts.map +0 -1
  67. package/dist/bin/lint/package.js +0 -81
  68. package/dist/bin/lint/package.test.d.ts +0 -2
  69. package/dist/bin/lint/package.test.d.ts.map +0 -1
  70. package/dist/bin/lint/package.test.js +0 -65
  71. package/dist/bin/ship/hotfix.d.ts +0 -3
  72. package/dist/bin/ship/hotfix.d.ts.map +0 -1
  73. package/dist/bin/ship/hotfix.js +0 -141
  74. package/dist/bin/ship/production.d.ts +0 -3
  75. package/dist/bin/ship/production.d.ts.map +0 -1
  76. package/dist/bin/ship/production.js +0 -124
  77. package/dist/bin/ship/staging.d.ts +0 -3
  78. package/dist/bin/ship/staging.d.ts.map +0 -1
  79. package/dist/bin/ship/staging.js +0 -51
  80. package/dist/bin/ship/utils.d.ts +0 -9
  81. package/dist/bin/ship/utils.d.ts.map +0 -1
  82. package/dist/bin/ship/utils.js +0 -63
  83. package/dist/bin/ship/utils.test.d.ts +0 -2
  84. package/dist/bin/ship/utils.test.d.ts.map +0 -1
  85. package/dist/bin/ship/utils.test.js +0 -127
  86. package/dist/config.test.d.ts +0 -2
  87. package/dist/config.test.d.ts.map +0 -1
  88. package/dist/config.test.js +0 -101
  89. package/dist/playwright/index.d.ts +0 -10
  90. package/dist/playwright/index.d.ts.map +0 -1
  91. package/dist/playwright/index.js +0 -42
  92. package/dist/playwright/index.test.d.ts +0 -2
  93. package/dist/playwright/index.test.d.ts.map +0 -1
  94. package/dist/playwright/index.test.js +0 -55
  95. package/dist/testing/setup-react.d.ts +0 -2
  96. package/dist/testing/setup-react.d.ts.map +0 -1
  97. package/dist/testing/setup-react.js +0 -1
  98. package/dist/vitest/node.d.ts +0 -22
  99. package/dist/vitest/node.d.ts.map +0 -1
  100. package/dist/vitest/node.js +0 -16
  101. package/dist/vitest/react.d.ts +0 -17
  102. package/dist/vitest/react.d.ts.map +0 -1
  103. package/dist/vitest/react.js +0 -12
  104. package/src/bin/exec/clean.test.ts +0 -63
  105. package/src/bin/exec/clean.ts +0 -36
  106. package/src/bin/exec/husky.ts +0 -14
  107. package/src/bin/exec/p.ts +0 -13
  108. package/src/bin/exec/s.ts +0 -13
  109. package/src/bin/exec/tsc.ts +0 -13
  110. package/src/bin/lint/biome.ts +0 -13
  111. package/src/bin/lint/commit.ts +0 -13
  112. package/src/bin/lint/md.ts +0 -28
  113. package/src/bin/lint/package.test.ts +0 -83
  114. package/src/bin/lint/package.ts +0 -108
  115. package/src/bin/ship/hotfix.ts +0 -241
  116. package/src/bin/ship/production.ts +0 -240
  117. package/src/bin/ship/staging.ts +0 -108
  118. package/src/bin/ship/utils.test.ts +0 -178
  119. package/src/bin/ship/utils.ts +0 -109
  120. package/src/config.test.ts +0 -129
  121. package/src/markdownlint/markdownlint-cli2.jsonc +0 -9
  122. package/src/playwright/index.test.ts +0 -73
  123. package/src/playwright/index.ts +0 -63
  124. package/src/templates/release.yml +0 -128
  125. package/src/testing/setup-react.ts +0 -8
  126. package/src/vitest/node.ts +0 -25
  127. package/src/vitest/react.ts +0 -19
  128. /package/{src → templates}/sqlfluff/setup.cfg +0 -0
@@ -1,240 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * ship-production: Version and promote main to production following the GitLab workflow.
4
- *
5
- * Usage: ship-production <patch|minor|major>
6
- *
7
- * GitLab workflow:
8
- * main → (version bump commit) → production → staging → main
9
- *
10
- * Versioning is intentionally deferred to this step so that version numbers
11
- * only ever correspond to production-verified code.
12
- *
13
- * This script:
14
- * 1. Ensures the current branch is main and the working tree is clean
15
- * 2. Verifies main is ahead of production (there is something to ship)
16
- * 3. Runs quality checks on main
17
- * 4. Bumps the version in package.json
18
- * 5. Collects change descriptions from git log between production and main
19
- * 6. Updates CHANGELOG.md
20
- * 7. Commits the version bump on main
21
- * 8. Merges main into production (fast-forward) and pushes
22
- * 9. Merges production into staging to keep it in sync
23
- * 10. Merges production back into main to ensure consistency
24
- * 11. Returns to main
25
- */
26
- import { execSync } from 'node:child_process';
27
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
28
- import { join } from 'node:path';
29
-
30
- import {
31
- branchExists,
32
- bumpVersion,
33
- confirm,
34
- git,
35
- gitRead,
36
- insertChangelog,
37
- runQualityChecks,
38
- runScript,
39
- } from './utils.js';
40
-
41
- const args = process.argv.slice(2);
42
- const bumpType = args[0];
43
-
44
- if (!bumpType || !['patch', 'minor', 'major'].includes(bumpType)) {
45
- console.error('Usage: ship-production <patch|minor|major>');
46
- console.error('Example: ship-production minor');
47
- process.exit(1);
48
- }
49
-
50
- // ---------------------------------------------------------------------------
51
- // Guard: must be on main
52
- // ---------------------------------------------------------------------------
53
- const currentBranch = gitRead('branch', '--show-current');
54
- if (currentBranch !== 'main') {
55
- console.error(`Must be on the main branch to ship. Currently on: ${currentBranch}`);
56
- process.exit(1);
57
- }
58
-
59
- // Guard: working tree must be clean
60
- const status = gitRead('status', '--porcelain');
61
- if (status) {
62
- console.error('Working directory has uncommitted changes. Commit or stash them first.');
63
- process.exit(1);
64
- }
65
-
66
- // ---------------------------------------------------------------------------
67
- // Fetch and verify both branches exist
68
- // ---------------------------------------------------------------------------
69
- console.log('\nFetching latest state from origin...');
70
- git('fetch', 'origin');
71
-
72
- if (!branchExists('staging')) {
73
- console.error(
74
- 'Branch "staging" does not exist. Create it first:\n'
75
- + ' git checkout -b staging && git push -u origin staging',
76
- );
77
- process.exit(1);
78
- }
79
-
80
- if (!branchExists('production')) {
81
- console.error(
82
- 'Branch "production" does not exist. Create it first:\n'
83
- + ' git checkout -b production && git push -u origin production',
84
- );
85
- process.exit(1);
86
- }
87
-
88
- // ---------------------------------------------------------------------------
89
- // Verify main has commits not yet in production
90
- // ---------------------------------------------------------------------------
91
- git('pull', '--ff-only', 'origin', 'main');
92
- const ahead = gitRead('log', 'origin/production..HEAD', '--oneline');
93
- if (!ahead) {
94
- console.error('main is already in sync with production. Nothing to ship.');
95
- process.exit(1);
96
- }
97
-
98
- console.log('\nCommits to be shipped to production:');
99
- console.log(ahead);
100
-
101
- // ---------------------------------------------------------------------------
102
- // Read package name for the confirmation prompt
103
- // ---------------------------------------------------------------------------
104
- const packageJsonPath = join(process.cwd(), 'package.json');
105
- if (!existsSync(packageJsonPath)) {
106
- console.error('No package.json found in current directory.');
107
- process.exit(1);
108
- }
109
- const { name: packageName } = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {
110
- name: string;
111
- };
112
-
113
- if (!confirm(`\nShip ${packageName} as a ${bumpType} release?`)) {
114
- console.log('Aborted.');
115
- process.exit(0);
116
- }
117
-
118
- // ---------------------------------------------------------------------------
119
- // Quality checks on main
120
- // ---------------------------------------------------------------------------
121
- console.log('\nRunning quality checks on main...');
122
- try {
123
- runQualityChecks();
124
- } catch {
125
- console.error('\nQuality checks failed on main. Fix issues before shipping.');
126
- process.exit(1);
127
- }
128
- console.log('✅ Quality checks passed');
129
-
130
- // ---------------------------------------------------------------------------
131
- // Read version from main package.json and bump
132
- // ---------------------------------------------------------------------------
133
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {
134
- name: string;
135
- version: string;
136
- };
137
- const oldVersion = packageJson.version;
138
- const newVersion = bumpVersion(oldVersion, bumpType);
139
-
140
- console.log(`\nBumping ${packageName}: ${oldVersion} → ${newVersion}`);
141
-
142
- writeFileSync(
143
- packageJsonPath,
144
- `${JSON.stringify({ ...packageJson, version: newVersion }, null, 2)}\n`,
145
- );
146
-
147
- // ---------------------------------------------------------------------------
148
- // Derive change descriptions from git log between production and staging
149
- // ---------------------------------------------------------------------------
150
- const logOutput = gitRead('log', 'origin/production..HEAD', '--pretty=format:%s');
151
- const changeLines = logOutput
152
- ? logOutput
153
- .split('\n')
154
- .map((s) => s.trim())
155
- .filter((s) => s.length > 0)
156
- : [];
157
-
158
- const changeBody =
159
- changeLines.length > 0
160
- ? changeLines.map((c) => `- ${c.length > 98 ? `${c.slice(0, 95)}...` : c}`).join('\n')
161
- : `${bumpType} release`;
162
-
163
- // ---------------------------------------------------------------------------
164
- // Update CHANGELOG.md
165
- // ---------------------------------------------------------------------------
166
- const changelogPath = join(process.cwd(), 'CHANGELOG.md');
167
- const today = new Date().toISOString().slice(0, 10);
168
- insertChangelog(changelogPath, `## [${newVersion}] - ${today}\n\n${changeBody}\n`);
169
-
170
- // ---------------------------------------------------------------------------
171
- // Fix formatting of modified files (package.json, CHANGELOG.md)
172
- // ---------------------------------------------------------------------------
173
- try {
174
- runScript('fix:pkg');
175
- } catch {
176
- // fix:pkg may not exist
177
- }
178
-
179
- // Format modified files
180
- try {
181
- git('add', '-A');
182
- const changedFiles = gitRead('diff', '--cached', '--name-only').split('\n').filter(Boolean);
183
-
184
- // Format JSON files with biome
185
- for (const file of changedFiles) {
186
- if (file.endsWith('.json')) {
187
- try {
188
- execSync(`npx biome check --write ${file}`, { cwd: process.cwd(), stdio: 'inherit' });
189
- } catch {
190
- // File might not need formatting or biome not available
191
- }
192
- }
193
- }
194
-
195
- // Format markdown files with markdownlint
196
- for (const file of changedFiles) {
197
- if (file.endsWith('.md')) {
198
- try {
199
- execSync(`npx markdownlint-cli2 --fix ${file}`, { cwd: process.cwd(), stdio: 'inherit' });
200
- } catch {
201
- // File might not need formatting or markdownlint not available
202
- }
203
- }
204
- }
205
- } catch {
206
- // Formatters not available
207
- }
208
-
209
- // ---------------------------------------------------------------------------
210
- // Commit version bump on main
211
- // ---------------------------------------------------------------------------
212
- git('add', '-A');
213
- git('commit', '-m', `chore(release): ${packageName}@${newVersion}`, '-m', changeBody);
214
-
215
- // ---------------------------------------------------------------------------
216
- // Merge main → production
217
- // ---------------------------------------------------------------------------
218
- console.log('\nMerging main into production...');
219
- git('checkout', 'production');
220
- git('pull', '--ff-only', 'origin', 'production');
221
- git('merge', '--ff-only', 'main');
222
- git('push', 'origin', 'production');
223
-
224
- // ---------------------------------------------------------------------------
225
- // Sync staging with production
226
- // ---------------------------------------------------------------------------
227
- console.log('\nSyncing staging with production...');
228
- git('checkout', 'staging');
229
- git('pull', '--ff-only', 'origin', 'staging');
230
- git('merge', '--ff-only', 'production');
231
- git('push', 'origin', 'staging');
232
-
233
- // ---------------------------------------------------------------------------
234
- // Return to main and push
235
- // ---------------------------------------------------------------------------
236
- git('checkout', 'main');
237
- git('push', 'origin', 'main');
238
-
239
- console.log(`\n✅ Shipped ${packageName}@${newVersion} to production`);
240
- console.log('You are on main and ready to keep working.');
@@ -1,108 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * ship-staging: Deploy changes to staging following the GitLab workflow.
4
- *
5
- * Usage: ship-staging [message]
6
- *
7
- * GitLab workflow:
8
- * main → staging (staging deploy, no version bump yet)
9
- *
10
- * This script is OPTIONAL. You can ship directly to production using ship-production,
11
- * which will automatically sync staging afterward.
12
- *
13
- * Use ship-staging when you want to test changes in a staging environment before
14
- * shipping to production. Otherwise, skip this step and use ship-production directly.
15
- *
16
- * Versioning happens at ship time (ship-production), not here.
17
- * This keeps version numbers reserved for production-verified code.
18
- *
19
- * This script:
20
- * 1. Ensures the current branch is main and the working tree is clean
21
- * 2. Pulls latest main from origin
22
- * 3. Runs quality checks locally (build, lint, typecheck, tests)
23
- * 4. Merges main into staging (fast-forward) and pushes
24
- * 5. Pushes main and returns so work can continue
25
- */
26
- import { existsSync, readFileSync } from 'node:fs';
27
- import { join } from 'node:path';
28
-
29
- import { branchExists, git, gitRead, runQualityChecks } from './utils.js';
30
-
31
- // ---------------------------------------------------------------------------
32
- // Guard: must be on main
33
- // ---------------------------------------------------------------------------
34
- const currentBranch = gitRead('branch', '--show-current');
35
- if (currentBranch !== 'main') {
36
- console.error(`Must be on the main branch to release. Currently on: ${currentBranch}`);
37
- process.exit(1);
38
- }
39
-
40
- // Guard: working tree must be clean
41
- const status = gitRead('status', '--porcelain');
42
- if (status) {
43
- console.error('Working directory has uncommitted changes. Commit or stash them first.');
44
- process.exit(1);
45
- }
46
-
47
- // ---------------------------------------------------------------------------
48
- // Sync with origin before doing anything
49
- // ---------------------------------------------------------------------------
50
- console.log('\nFetching latest state from origin...');
51
- git('fetch', 'origin');
52
- git('pull', '--ff-only', 'origin', 'main');
53
-
54
- // ---------------------------------------------------------------------------
55
- // Read package.json
56
- // ---------------------------------------------------------------------------
57
- const packageJsonPath = join(process.cwd(), 'package.json');
58
- if (!existsSync(packageJsonPath)) {
59
- console.error('No package.json found in current directory.');
60
- process.exit(1);
61
- }
62
-
63
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as { name: string };
64
- const packageName = packageJson.name;
65
-
66
- if (!packageName) {
67
- console.error('No "name" field found in package.json.');
68
- process.exit(1);
69
- }
70
-
71
- // Guard: staging branch must exist
72
- if (!branchExists('staging')) {
73
- console.error(
74
- 'Branch "staging" does not exist locally or on origin. Create it first:\n'
75
- + ' git checkout -b staging && git push -u origin staging',
76
- );
77
- process.exit(1);
78
- }
79
-
80
- // ---------------------------------------------------------------------------
81
- // Quality checks — no broken code may be committed
82
- // ---------------------------------------------------------------------------
83
- console.log('\nRunning quality checks...');
84
- try {
85
- runQualityChecks();
86
- } catch {
87
- console.error('\nQuality checks failed. Fix all issues before releasing.');
88
- process.exit(1);
89
- }
90
- console.log('✅ Quality checks passed');
91
-
92
- // ---------------------------------------------------------------------------
93
- // Merge into staging
94
- // ---------------------------------------------------------------------------
95
- console.log('\nMerging main into staging...');
96
- git('checkout', 'staging');
97
- git('merge', '--ff-only', 'main');
98
- git('push', 'origin', 'staging');
99
-
100
- // ---------------------------------------------------------------------------
101
- // Return to main and sync
102
- // ---------------------------------------------------------------------------
103
- git('checkout', 'main');
104
- git('push', 'origin', 'main');
105
-
106
- console.log('\n✅ Changes deployed to staging');
107
- console.log('Run ship-production <patch|minor|major> when ready to ship to production.');
108
- console.log('(Or ship directly from main to production without using ship-staging first.)');
@@ -1,178 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
-
6
- import { bumpVersion, confirm, insertChangelog } from './utils.js';
7
-
8
- // ---------------------------------------------------------------------------
9
- // bumpVersion
10
- // ---------------------------------------------------------------------------
11
-
12
- describe('bumpVersion', () => {
13
- it('bumps patch', () => {
14
- expect(bumpVersion('1.2.3', 'patch')).toBe('1.2.4');
15
- });
16
-
17
- it('bumps minor and resets patch', () => {
18
- expect(bumpVersion('1.2.3', 'minor')).toBe('1.3.0');
19
- });
20
-
21
- it('bumps major and resets minor + patch', () => {
22
- expect(bumpVersion('1.2.3', 'major')).toBe('2.0.0');
23
- });
24
-
25
- it('handles zero components', () => {
26
- expect(bumpVersion('0.0.0', 'patch')).toBe('0.0.1');
27
- expect(bumpVersion('0.0.0', 'minor')).toBe('0.1.0');
28
- expect(bumpVersion('0.0.0', 'major')).toBe('1.0.0');
29
- });
30
-
31
- it('defaults to patch for unknown bump type', () => {
32
- expect(bumpVersion('1.2.3', 'unknown')).toBe('1.2.4');
33
- });
34
-
35
- it('throws for invalid semver', () => {
36
- expect(() => bumpVersion('not-semver', 'patch')).toThrow('Invalid semver');
37
- expect(() => bumpVersion('1.2', 'patch')).toThrow('Invalid semver');
38
- expect(() => bumpVersion('1.2.x', 'patch')).toThrow('Invalid semver');
39
- });
40
- });
41
-
42
- // ---------------------------------------------------------------------------
43
- // insertChangelog
44
- // ---------------------------------------------------------------------------
45
-
46
- describe('insertChangelog', () => {
47
- let tmpDir: string;
48
- let changelogPath: string;
49
-
50
- beforeEach(() => {
51
- tmpDir = join(tmpdir(), `flow-utils-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
52
- mkdirSync(tmpDir, { recursive: true });
53
- changelogPath = join(tmpDir, 'CHANGELOG.md');
54
- });
55
-
56
- afterEach(() => {
57
- rmSync(tmpDir, { force: true, recursive: true });
58
- });
59
-
60
- it('creates a new file when none exists', () => {
61
- insertChangelog(changelogPath, '## [1.0.0] - 2025-01-01\n\n- initial release\n');
62
-
63
- expect(existsSync(changelogPath)).toBe(true);
64
- const content = readFileSync(changelogPath, 'utf-8');
65
- expect(content).toContain('# Changelog');
66
- expect(content).toContain('## [1.0.0] - 2025-01-01');
67
- });
68
-
69
- it('inserts before the first existing ## section', () => {
70
- writeFileSync(changelogPath, '# Changelog\n\n## [1.0.0] - 2025-01-01\n\n- old entry\n');
71
-
72
- insertChangelog(changelogPath, '## [1.1.0] - 2025-02-01\n\n- new entry\n');
73
-
74
- const content = readFileSync(changelogPath, 'utf-8');
75
- const newIdx = content.indexOf('## [1.1.0]');
76
- const oldIdx = content.indexOf('## [1.0.0]');
77
- expect(newIdx).toBeLessThan(oldIdx);
78
- });
79
-
80
- it('appends when no ## section exists yet', () => {
81
- writeFileSync(changelogPath, '# Changelog\n');
82
-
83
- insertChangelog(changelogPath, '## [1.0.0] - 2025-01-01\n\n- initial\n');
84
-
85
- const content = readFileSync(changelogPath, 'utf-8');
86
- expect(content).toContain('## [1.0.0]');
87
- });
88
-
89
- it('preserves existing entries when inserting', () => {
90
- writeFileSync(changelogPath, '# Changelog\n\n## [1.0.0] - 2025-01-01\n\n- old entry\n');
91
-
92
- insertChangelog(changelogPath, '## [1.1.0] - 2025-02-01\n\n- new entry\n');
93
-
94
- const content = readFileSync(changelogPath, 'utf-8');
95
- expect(content).toContain('## [1.0.0]');
96
- expect(content).toContain('- old entry');
97
- expect(content).toContain('## [1.1.0]');
98
- expect(content).toContain('- new entry');
99
- });
100
-
101
- it('appends when file exists but has no title line', () => {
102
- writeFileSync(changelogPath, '## [1.0.0] - 2025-01-01\n\n- old entry\n');
103
-
104
- insertChangelog(changelogPath, '## [1.1.0] - 2025-02-01\n\n- new entry\n');
105
-
106
- const content = readFileSync(changelogPath, 'utf-8');
107
- expect(content).toContain('## [1.0.0]');
108
- expect(content).toContain('## [1.1.0]');
109
- });
110
-
111
- it('handles multiple existing entries in the correct order', () => {
112
- writeFileSync(
113
- changelogPath,
114
- '# Changelog\n\n## [1.1.0] - 2025-02-01\n\n- second\n\n## [1.0.0] - 2025-01-01\n\n- first\n',
115
- );
116
-
117
- insertChangelog(changelogPath, '## [1.2.0] - 2025-03-01\n\n- third\n');
118
-
119
- const content = readFileSync(changelogPath, 'utf-8');
120
- const idx120 = content.indexOf('## [1.2.0]');
121
- const idx110 = content.indexOf('## [1.1.0]');
122
- const idx100 = content.indexOf('## [1.0.0]');
123
- expect(idx120).toBeLessThan(idx110);
124
- expect(idx110).toBeLessThan(idx100);
125
- });
126
- });
127
-
128
- // ---------------------------------------------------------------------------
129
- // confirm
130
- // ---------------------------------------------------------------------------
131
-
132
- describe('confirm', () => {
133
- let tmpDir: string;
134
- let inputFile: string;
135
-
136
- beforeEach(() => {
137
- tmpDir = join(tmpdir(), `confirm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
138
- mkdirSync(tmpDir, { recursive: true });
139
- inputFile = join(tmpDir, 'input');
140
- vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
141
- });
142
-
143
- afterEach(() => {
144
- rmSync(tmpDir, { force: true, recursive: true });
145
- vi.restoreAllMocks();
146
- });
147
-
148
- it('returns true for "y"', () => {
149
- writeFileSync(inputFile, 'y\n');
150
- expect(confirm('Continue?', inputFile)).toBe(true);
151
- });
152
-
153
- it('returns true for "Y"', () => {
154
- writeFileSync(inputFile, 'Y\n');
155
- expect(confirm('Continue?', inputFile)).toBe(true);
156
- });
157
-
158
- it('returns false for "n"', () => {
159
- writeFileSync(inputFile, 'n\n');
160
- expect(confirm('Continue?', inputFile)).toBe(false);
161
- });
162
-
163
- it('returns false for empty input', () => {
164
- writeFileSync(inputFile, '\n');
165
- expect(confirm('Continue?', inputFile)).toBe(false);
166
- });
167
-
168
- it('returns false for any other input', () => {
169
- writeFileSync(inputFile, 'yes\n');
170
- expect(confirm('Continue?', inputFile)).toBe(false);
171
- });
172
-
173
- it('writes the prompt to stdout', () => {
174
- writeFileSync(inputFile, 'y\n');
175
- confirm('Ship it?', inputFile);
176
- expect(process.stdout.write).toHaveBeenCalledWith('Ship it? (y/N) ');
177
- });
178
- });
@@ -1,109 +0,0 @@
1
- /**
2
- * Shared utilities for ship-staging, ship-production, and ship-hotfix.
3
- *
4
- * Git commands use execFileSync (not a shell string) so user-provided
5
- * strings such as commit messages are never interpolated by the shell.
6
- * pnpm script invocations use execSync via shell since script names are
7
- * developer-controlled and pnpm itself is resolved through PATH.
8
- */
9
- import { execFileSync, execSync } from 'node:child_process';
10
- import { closeSync, existsSync, openSync, readFileSync, readSync, writeFileSync } from 'node:fs';
11
-
12
- /**
13
- * Run a git command with the given arguments.
14
- * Output is inherited (visible to the user).
15
- */
16
- export const git = (...args: string[]): void => {
17
- console.log(`$ git ${args.join(' ')}`);
18
- execFileSync('git', args, { stdio: 'inherit' });
19
- };
20
-
21
- /**
22
- * Run a git command and return trimmed stdout.
23
- * Stderr is suppressed; throws on non-zero exit.
24
- */
25
- export const gitRead = (...args: string[]): string =>
26
- execFileSync('git', args, { encoding: 'utf-8' }).trim();
27
-
28
- /**
29
- * Run a pnpm script via the shell.
30
- * Only pass developer-controlled script names — never user input.
31
- */
32
- export const runScript = (script: string): void => {
33
- console.log(`$ pnpm ${script}`);
34
- execSync(`pnpm ${script}`, { stdio: 'inherit' });
35
- };
36
-
37
- /**
38
- * Bump a semver string by the given increment type.
39
- */
40
- export const bumpVersion = (current: string, bump: string): string => {
41
- const parts = current.split('.').map(Number);
42
- if (parts.length !== 3 || parts.some(Number.isNaN)) {
43
- throw new Error(`Invalid semver: ${current}`);
44
- }
45
- const [major, minor, patch] = parts as [number, number, number];
46
- if (bump === 'major') return `${major + 1}.0.0`;
47
- if (bump === 'minor') return `${major}.${minor + 1}.0`;
48
- return `${major}.${minor}.${patch + 1}`;
49
- };
50
-
51
- /**
52
- * Insert a new changelog entry after the title line of CHANGELOG.md.
53
- * Creates the file if it does not exist.
54
- */
55
- export const insertChangelog = (changelogPath: string, entry: string): void => {
56
- if (existsSync(changelogPath)) {
57
- const existing = readFileSync(changelogPath, 'utf-8');
58
- const insertAt = existing.indexOf('\n## ');
59
- if (insertAt === -1) {
60
- writeFileSync(changelogPath, `${existing.trimEnd()}\n\n${entry}`);
61
- } else {
62
- writeFileSync(
63
- changelogPath,
64
- `${existing.slice(0, insertAt)}\n\n${entry}${existing.slice(insertAt + 1)}`,
65
- );
66
- }
67
- } else {
68
- writeFileSync(changelogPath, `# Changelog\n\n${entry}`);
69
- }
70
- };
71
-
72
- /**
73
- * Run quality checks. Throws if any step fails.
74
- */
75
- export const runQualityChecks = (): void => {
76
- runScript('build');
77
- runScript('typecheck');
78
- runScript('test');
79
- };
80
-
81
- /**
82
- * Check whether a branch exists locally or on origin.
83
- * Requires a prior `git fetch` to have up-to-date remote refs.
84
- */
85
- export const branchExists = (name: string): boolean =>
86
- gitRead('branch', '--list', name) !== ''
87
- || gitRead('branch', '--list', '--remotes', `origin/${name}`) !== '';
88
-
89
- /**
90
- * Prompt the user for confirmation and return true only if they type "y" or "Y".
91
- * Defaults to "no" on empty input, so pressing Enter alone aborts.
92
- * The ttyPath parameter is injectable for testing.
93
- */
94
- export const confirm = (prompt: string, ttyPath = '/dev/tty'): boolean => {
95
- process.stdout.write(`${prompt} (y/N) `);
96
- const buf = Buffer.alloc(1024);
97
- let fd: number;
98
- let shouldClose = false;
99
- try {
100
- fd = openSync(ttyPath, 'r');
101
- shouldClose = true;
102
- } catch {
103
- fd = process.stdin.fd;
104
- }
105
- const bytesRead = readSync(fd, buf, 0, buf.length, null);
106
- if (shouldClose) closeSync(fd);
107
- const answer = buf.slice(0, bytesRead).toString().trim();
108
- return answer === 'y' || answer === 'Y';
109
- };