@@ -27,18 +27,20 @@ function ok(msg) {
2727
2828function run ( cmd , args , opts = { } ) {
2929 const label = opts . label ?? `${ cmd } ${ args . join ( " " ) } ` ;
30- if ( opts . dryRun ) {
30+ if ( opts . dryRun && ! opts . runInDryRun ) {
3131 log ( ` (dry-run) ${ label } ` ) ;
3232 return { status : 0 , stdout : "" } ;
3333 }
3434 const result = spawnSync ( cmd , args , {
3535 stdio : opts . stdio ?? "inherit" ,
3636 cwd : opts . cwd ?? root ,
37- shell : true ,
37+ shell : false ,
38+ encoding : opts . encoding ,
3839 env : { ...process . env , ...opts . env } ,
3940 } ) ;
4041 if ( result . status !== 0 ) {
41- fail ( `Command failed: ${ label } ` ) ;
42+ const output = [ result . stdout , result . stderr ] . filter ( Boolean ) . join ( "\n" ) . trim ( ) ;
43+ fail ( `Command failed: ${ label } ${ output ? `\n${ output } ` : "" } ` ) ;
4244 }
4345 return result ;
4446}
@@ -55,6 +57,70 @@ function isValidSemver(v) {
5557 return / ^ \d + \. \d + \. \d + ( - [ \w . ] + ) ? ( \+ [ \w . ] + ) ? $ / . test ( v ) ;
5658}
5759
60+ function isValidNpmTag ( v ) {
61+ return / ^ [ A - Z a - z 0 - 9 ] [ A - Z a - z 0 - 9 . _ - ] * $ / . test ( v ) ;
62+ }
63+
64+ function hasPackFile ( files , expectedPath ) {
65+ return files . includes ( expectedPath ) ;
66+ }
67+
68+ function hasPackPrefix ( files , expectedPrefix ) {
69+ return files . some ( ( file ) => file . startsWith ( expectedPrefix ) ) ;
70+ }
71+
72+ function validatePacklist ( cwd , checks , opts = { } ) {
73+ const label = opts . label ?? `npm pack --dry-run --json --ignore-scripts` ;
74+ const result = run ( "npm" , [ "pack" , "--dry-run" , "--json" , "--ignore-scripts" ] , {
75+ cwd,
76+ label,
77+ stdio : "pipe" ,
78+ encoding : "utf-8" ,
79+ runInDryRun : true ,
80+ } ) ;
81+ const output = result . stdout . trim ( ) ;
82+ const packs = JSON . parse ( output ) ;
83+ const pack = Array . isArray ( packs ) ? packs [ 0 ] : packs ;
84+ const files = ( pack ?. files ?? [ ] ) . map ( ( file ) => file . path ) ;
85+ const missing = [ ] ;
86+
87+ for ( const check of checks ) {
88+ const found = check . type === "prefix" ? hasPackPrefix ( files , check . value ) : hasPackFile ( files , check . value ) ;
89+ if ( ! found ) {
90+ missing . push ( check . label ?? check . value ) ;
91+ }
92+ }
93+
94+ if ( missing . length > 0 ) {
95+ fail ( `Package tarball is missing required files:\n - ${ missing . join ( "\n - " ) } ` ) ;
96+ }
97+
98+ ok ( `Validated package tarball (${ files . length } files)` ) ;
99+ }
100+
101+ function hasGitChanges ( paths ) {
102+ const result = spawnSync ( "git" , [ "diff" , "--quiet" , "--" , ...paths ] , {
103+ cwd : root ,
104+ shell : false ,
105+ } ) ;
106+ if ( result . status === 0 ) {
107+ return false ;
108+ }
109+ if ( result . status === 1 ) {
110+ return true ;
111+ }
112+ fail ( "Unable to check release file changes." ) ;
113+ }
114+
115+ function gitTagExists ( tagName ) {
116+ const result = spawnSync ( "git" , [ "rev-parse" , "-q" , "--verify" , `refs/tags/${ tagName } ` ] , {
117+ cwd : root ,
118+ shell : false ,
119+ stdio : "ignore" ,
120+ } ) ;
121+ return result . status === 0 ;
122+ }
123+
58124// ── Parse args ───────────────────────────────────────────────────────────────
59125
60126const args = process . argv . slice ( 2 ) ;
@@ -103,6 +169,10 @@ if (!isValidSemver(version)) {
103169 fail ( `Invalid semver version: ${ version } ` ) ;
104170}
105171
172+ if ( ! isValidNpmTag ( tag ) ) {
173+ fail ( `Invalid npm dist-tag: ${ tag } ` ) ;
174+ }
175+
106176const TOTAL_STEPS = 8 ;
107177
108178// ── Banner ───────────────────────────────────────────────────────────────────
@@ -119,7 +189,7 @@ step(1, TOTAL_STEPS, "Checking git state...");
119189const gitStatus = spawnSync ( "git" , [ "status" , "--porcelain" ] , {
120190 cwd : root ,
121191 encoding : "utf-8" ,
122- shell : true ,
192+ shell : false ,
123193} ) ;
124194if ( gitStatus . stdout . trim ( ) ) {
125195 fail ( "Working tree is not clean. Commit or stash changes first." ) ;
@@ -130,7 +200,7 @@ if (!force) {
130200 const gitBranch = spawnSync ( "git" , [ "branch" , "--show-current" ] , {
131201 cwd : root ,
132202 encoding : "utf-8" ,
133- shell : true ,
203+ shell : false ,
134204 } ) ;
135205 const branch = gitBranch . stdout . trim ( ) ;
136206 if ( branch !== "main" ) {
@@ -147,7 +217,7 @@ if (!dryRun) {
147217 const whoami = spawnSync ( "npm" , [ "whoami" ] , {
148218 cwd : root ,
149219 encoding : "utf-8" ,
150- shell : true ,
220+ shell : false ,
151221 } ) ;
152222 if ( whoami . status !== 0 ) {
153223 fail ( "Not logged in to npm. Run `npm login` first." ) ;
@@ -182,6 +252,12 @@ if (!dryRun) {
182252 log ( ` (dry-run) packages/cli: ${ oldVersion } → ${ version } ` ) ;
183253}
184254
255+ run ( "npm" , [ "install" , "--package-lock-only" , "--ignore-scripts" ] , {
256+ dryRun,
257+ label : "npm install --package-lock-only --ignore-scripts" ,
258+ } ) ;
259+ ok ( "package-lock.json is up to date" ) ;
260+
185261// ── 4. Quality checks ────────────────────────────────────────────────────────
186262
187263step ( 4 , TOTAL_STEPS , "Running quality checks (typecheck + lint + format)..." ) ;
@@ -209,6 +285,7 @@ const cliRoot = join(root, "packages", "cli");
209285const distDir = join ( cliRoot , "dist" ) ;
210286const distCliJs = join ( distDir , "cli.js" ) ;
211287const distChunks = join ( distDir , "chunks" ) ;
288+ const distTemplates = join ( distDir , "templates" ) ;
212289const distBundled = join ( distDir , "bundled" ) ;
213290
214291if ( ! existsSync ( distCliJs ) ) {
@@ -217,10 +294,24 @@ if (!existsSync(distCliJs)) {
217294if ( ! existsSync ( distChunks ) ) {
218295 fail ( `Chunks directory not found: ${ distChunks } . Run "npm run build" first.` ) ;
219296}
297+ if ( ! existsSync ( distTemplates ) ) {
298+ fail ( `Templates directory not found: ${ distTemplates } . Run "npm run build" first.` ) ;
299+ }
220300if ( ! existsSync ( distBundled ) ) {
221301 fail ( `Bundled assets not found: ${ distBundled } . Run "npm run build" first.` ) ;
222302}
223303
304+ validatePacklist (
305+ cliRoot ,
306+ [
307+ { type : "file" , value : "dist/cli.js" } ,
308+ { type : "prefix" , value : "dist/chunks/" , label : "dist/chunks/*.js" } ,
309+ { type : "prefix" , value : "dist/templates/" , label : "dist/templates/**" } ,
310+ { type : "prefix" , value : "dist/bundled/" , label : "dist/bundled/**" } ,
311+ ] ,
312+ { label : "cd packages/cli && npm pack --dry-run --json --ignore-scripts" }
313+ ) ;
314+
224315// Copy README.md and LICENSE into dist/
225316for ( const file of [ "README.md" , "LICENSE" ] ) {
226317 const src = join ( root , file ) ;
@@ -258,9 +349,53 @@ if (!dryRun) {
258349}
259350log ( " Written dist/package.json with dependencies: {}" ) ;
260351
352+ if ( ! dryRun ) {
353+ validatePacklist (
354+ distDir ,
355+ [
356+ { type : "file" , value : "cli.js" } ,
357+ { type : "prefix" , value : "chunks/" , label : "chunks/*.js" } ,
358+ { type : "prefix" , value : "templates/" , label : "templates/**" } ,
359+ { type : "prefix" , value : "bundled/" , label : "bundled/**" } ,
360+ ] ,
361+ { label : "cd dist && npm pack --dry-run --json --ignore-scripts" }
362+ ) ;
363+ } else {
364+ log ( " (dry-run) skipped dist/package.json tarball validation because dist/package.json was not written" ) ;
365+ }
366+
261367ok ( "dist/ prepared for publishing" ) ;
262368
263- // ── 7. Publish from dist/ ────────────────────────────────────────────────────
369+ // ── Git commit + tag ─────────────────────────────────────────────────────────
370+
371+ const releaseFiles = [ "packages/core/package.json" , "packages/cli/package.json" , "package-lock.json" ] ;
372+ const tagName = `v${ version } ` ;
373+
374+ if ( ! dryRun ) {
375+ log ( "\nCreating git commit and tag..." ) ;
376+ if ( hasGitChanges ( releaseFiles ) ) {
377+ run ( "git" , [ "add" , ...releaseFiles ] , {
378+ label : "git add packages/*/package.json package-lock.json" ,
379+ } ) ;
380+ run ( "git" , [ "commit" , "-m" , `chore(release): v${ version } ` ] , {
381+ label : `git commit -m "chore(release): v${ version } "` ,
382+ } ) ;
383+ } else {
384+ log ( " No release file changes to commit; tagging current HEAD" ) ;
385+ }
386+
387+ if ( gitTagExists ( tagName ) ) {
388+ fail ( `Git tag already exists: ${ tagName } ` ) ;
389+ }
390+ run ( "git" , [ "tag" , tagName ] , {
391+ label : `git tag ${ tagName } ` ,
392+ } ) ;
393+ ok ( `Created tag ${ tagName } ` ) ;
394+ } else {
395+ log ( "\n (dry-run) git add + commit + tag" ) ;
396+ }
397+
398+ // ── 8. Publish from dist/ ────────────────────────────────────────────────────
264399
265400step ( 8 , TOTAL_STEPS , "Publishing @vegamo/deepcode-cli from dist/..." ) ;
266401
@@ -274,24 +409,6 @@ run("npm", publishArgs, {
274409} ) ;
275410ok ( `Published @vegamo/deepcode-cli@${ version } ` ) ;
276411
277- // ── Git commit + tag ─────────────────────────────────────────────────────────
278-
279- if ( ! dryRun ) {
280- log ( "\nCreating git commit and tag..." ) ;
281- run ( "git" , [ "add" , "packages/core/package.json" , "packages/cli/package.json" ] , {
282- label : "git add packages/*/package.json" ,
283- } ) ;
284- run ( "git" , [ "commit" , "-m" , `chore(release): v${ version } ` ] , {
285- label : `git commit -m "chore(release): v${ version } "` ,
286- } ) ;
287- run ( "git" , [ "tag" , `v${ version } ` ] , {
288- label : `git tag v${ version } ` ,
289- } ) ;
290- ok ( `Created commit and tag v${ version } ` ) ;
291- } else {
292- log ( "\n (dry-run) git add + commit + tag" ) ;
293- }
294-
295412// ── Done ─────────────────────────────────────────────────────────────────────
296413
297414console . log ( "\n=========================================" ) ;
0 commit comments