@edcalderon/versioning 1.4.7 โ†’ 1.5.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/CHANGELOG.md CHANGED
@@ -1,3 +1,5 @@
1
+
2
+
1
3
  # Changelog
2
4
 
3
5
  All notable changes to this project will be documented in this file.
@@ -5,6 +7,27 @@ All notable changes to this project will be documented in this file.
5
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
8
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
9
 
10
+ ## [1.5.1](https://github.com/edcalderon/my-second-brain/tree/main/packages/versioning) (2026-03-19)
11
+
12
+ ### Fixed
13
+ - ๐Ÿ”ง Fixed `secrets-check` extension to use Husky v9 hook format (removed deprecated `._/husky.sh` sourcing)
14
+ - ๐Ÿ”ง Fixed `cleanup-repo` extension to use Husky v9 hook format
15
+ - ๐Ÿ”„ Enhanced `init` command to automatically set up husky and add `prepare` script to package.json
16
+ - โœจ Added optional `postinstall` hook to versioning package that conditionally sets up husky when consumed
17
+
18
+ ### Changed
19
+ - ๐Ÿ“ Updated hook generation to be compatible with Husky v9+ (eliminates deprecation warnings in v10)
20
+
21
+ ## [1.5.0](https://github.com/edcalderon/my-second-brain/tree/main/packages/versioning) (2026-03-19)
22
+
23
+ ### Added
24
+ - โœจ New `workspace-env` extension (v1.0.0)
25
+ - `versioning env sync` to generate per-target `.env.local` and `.env.example` files from one canonical manifest
26
+ - `versioning env doctor` to report missing required variables and unknown root env keys
27
+ - `versioning env validate` for CI-friendly required variable validation with non-zero exit on missing vars
28
+ - Supports manifest sources, aliases, canonical variable metadata, and target key mapping
29
+ - ๐Ÿงช Added unit coverage for env parsing, sync generation, validation logic, and command registration
30
+
8
31
  ## [1.4.7](https://github.com/edcalderon/my-second-brain/tree/main/packages/versioning) (2026-03-16)
9
32
 
10
33
  ### Added
package/README.md CHANGED
@@ -8,19 +8,16 @@ A comprehensive versioning and changelog management tool designed for monorepos
8
8
 
9
9
  ---
10
10
 
11
- ## ๐Ÿ“‹ Latest Changes (v1.4.7)
12
-
13
- ### Added
14
- - โœจ New `workspace-scripts` extension (v1.0.0)
15
- - `versioning scripts sync` โ€” auto-generate `dev:all`, `build:all`, and per-app scripts in root package.json
16
- - `versioning scripts list` โ€” display current workspace script configuration
17
- - `versioning scripts detect` โ€” find new workspace apps not yet tracked in config
18
- - `versioning scripts preview` โ€” preview generated scripts without writing
19
- - ๐Ÿ”„ `postSync` hook auto-detects new apps added to pnpm-workspace.yaml
20
- - โš™๏ธ Config-driven via `extensionConfig.workspace-scripts` in versioning.config.json
21
- - ๐Ÿƒ Runner support: `concurrently` (default) or `turbo`
22
- - ๐Ÿ“ฆ Managed script tracking: safely adds/updates/removes only scripts it owns
23
- - ๐Ÿงช Comprehensive test suite for workspace-scripts extension
11
+ ## ๐Ÿ“‹ Latest Changes (v1.5.1)
12
+
13
+ ### Fixed
14
+ - ๐Ÿ”ง Fixed `secrets-check` extension to use Husky v9 hook format (removed deprecated `._/husky.sh` sourcing)
15
+ - ๐Ÿ”ง Fixed `cleanup-repo` extension to use Husky v9 hook format
16
+ - ๐Ÿ”„ Enhanced `init` command to automatically set up husky and add `prepare` script to package.json
17
+ - โœจ Added optional `postinstall` hook to versioning package that conditionally sets up husky when consumed
18
+
19
+ ### Changed
20
+ - ๐Ÿ“ Updated hook generation to be compatible with Husky v9+ (eliminates deprecation warnings in v10)
24
21
 
25
22
  For full version history, see [CHANGELOG.md](./CHANGELOG.md) and [GitHub releases](https://github.com/edcalderon/my-second-brain/releases)
26
23
 
@@ -114,6 +111,111 @@ Extensions can hook into the versioning lifecycle:
114
111
 
115
112
  ### Built-in Extensions
116
113
 
114
+ #### Workspace Env Extension
115
+
116
+ Adds first-class workspace environment orchestration for monorepos:
117
+
118
+ ```bash
119
+ versioning env sync
120
+ versioning env doctor
121
+ versioning env validate --target landing
122
+ ```
123
+
124
+ Key capabilities:
125
+ - Canonical root env sources: `.env` then `.env.local`
126
+ - Alias resolution for legacy variable names
127
+ - Minimal per-target runtime env generation
128
+ - Root and per-target example file generation from one manifest
129
+ - Deterministic output with generated headers and no rewrites when content is unchanged
130
+ - CI-friendly validation and dry-run sync checks
131
+
132
+ Manifest default lookup order:
133
+ - `config/env/manifest.ts`
134
+ - `config/env/manifest.cjs`
135
+ - `config/env/manifest.js`
136
+ - `config/env/manifest.json`
137
+
138
+ Supported manifest styles:
139
+ - Legacy map-based manifest:
140
+
141
+ ```ts
142
+ {
143
+ sources: ['.env', '.env.local'],
144
+ variables: {
145
+ SUPABASE_URL: {
146
+ description: 'Supabase URL',
147
+ required: true,
148
+ targets: {
149
+ landing: 'NEXT_PUBLIC_SUPABASE_URL'
150
+ }
151
+ }
152
+ },
153
+ targets: {
154
+ landing: {
155
+ path: 'apps/landing'
156
+ }
157
+ }
158
+ }
159
+ ```
160
+
161
+ - Spec-style manifest:
162
+
163
+ ```ts
164
+ {
165
+ rootExampleFile: '.env.example',
166
+ variables: [
167
+ {
168
+ key: 'SUPABASE_URL',
169
+ description: 'Supabase URL'
170
+ },
171
+ {
172
+ key: 'OPENAI_API_KEY',
173
+ aliases: ['LEGACY_OPENAI_KEY'],
174
+ secret: true
175
+ }
176
+ ],
177
+ targets: [
178
+ {
179
+ id: 'landing',
180
+ outputFile: 'apps/landing/.env.local',
181
+ exampleFile: 'apps/landing/.env.example',
182
+ entries: [
183
+ {
184
+ source: 'SUPABASE_URL',
185
+ target: 'NEXT_PUBLIC_SUPABASE_URL',
186
+ required: true
187
+ },
188
+ {
189
+ source: 'OPENAI_API_KEY'
190
+ }
191
+ ]
192
+ }
193
+ ]
194
+ }
195
+ ```
196
+
197
+ CLI options:
198
+
199
+ ```bash
200
+ versioning env sync --target landing --check
201
+ versioning env sync --json
202
+ versioning env doctor --target landing --json
203
+ versioning env validate --all --strict-unused
204
+ ```
205
+
206
+ Basic config (`versioning.config.json`):
207
+
208
+ ```json
209
+ {
210
+ "extensionConfig": {
211
+ "workspace-env": {
212
+ "enabled": true,
213
+ "manifestPath": "config/env/manifest.cjs"
214
+ }
215
+ }
216
+ }
217
+ ```
218
+
117
219
  #### Lifecycle Hooks Extension
118
220
 
119
221
  Demonstrates all available hooks with example business logic:
package/dist/cli.js CHANGED
@@ -35,7 +35,9 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  const commander_1 = require("commander");
38
+ const path = __importStar(require("path"));
38
39
  const fs = __importStar(require("fs-extra"));
40
+ const child_process_1 = require("child_process");
39
41
  const versioning_1 = require("./versioning");
40
42
  const changelog_1 = require("./changelog");
41
43
  const sync_1 = require("./sync");
@@ -406,6 +408,26 @@ program
406
408
  };
407
409
  await fs.writeJson(configPath, defaultConfig, { spaces: 2 });
408
410
  console.log('โœ… Initialized versioning config at versioning.config.json');
411
+ // Set up husky if in a git repo with a package.json
412
+ const pkgJsonPath = path.resolve('package.json');
413
+ if (await fs.pathExists(pkgJsonPath)) {
414
+ const pkgJson = await fs.readJson(pkgJsonPath);
415
+ // Add prepare script if missing
416
+ if (!pkgJson.scripts?.prepare) {
417
+ pkgJson.scripts = pkgJson.scripts || {};
418
+ pkgJson.scripts.prepare = 'husky';
419
+ await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
420
+ console.log('โœ… Added "prepare": "husky" to package.json');
421
+ }
422
+ // Run husky to set core.hooksPath
423
+ try {
424
+ (0, child_process_1.execSync)('npx husky', { stdio: 'ignore' });
425
+ console.log('โœ… Husky hooks initialized (.husky/)');
426
+ }
427
+ catch {
428
+ console.log('โš ๏ธ Could not initialize husky. Run "npx husky" manually.');
429
+ }
430
+ }
409
431
  }
410
432
  catch (error) {
411
433
  console.error('โŒ Error:', error instanceof Error ? error.message : String(error));
@@ -599,7 +599,6 @@ const extension = {
599
599
  else {
600
600
  const content = [
601
601
  '#!/bin/sh',
602
- '. "$(dirname "$0")/_/husky.sh"',
603
602
  '',
604
603
  cleanupBlock
605
604
  ].join('\n');
@@ -187,7 +187,6 @@ const extension = {
187
187
  else {
188
188
  const content = [
189
189
  '#!/bin/sh',
190
- '. "$(dirname "$0")/_/husky.sh"',
191
190
  '',
192
191
  block
193
192
  ].join('\n');
@@ -0,0 +1,80 @@
1
+ import { VersioningExtension } from '../../extensions';
2
+ export interface EnvTargetConfig {
3
+ path: string;
4
+ envFile?: string;
5
+ exampleFile?: string;
6
+ description?: string;
7
+ }
8
+ export interface EnvVariableConfig {
9
+ aliases?: string[];
10
+ description?: string;
11
+ secret?: boolean;
12
+ required?: boolean;
13
+ example?: string;
14
+ targets?: Record<string, string>;
15
+ }
16
+ export interface EnvVariableDefinition {
17
+ key: string;
18
+ aliases?: string[];
19
+ description?: string;
20
+ example?: string;
21
+ secret?: boolean;
22
+ }
23
+ export interface EnvTargetEntry {
24
+ source: string;
25
+ target?: string;
26
+ required?: boolean;
27
+ }
28
+ export interface EnvTargetDefinition {
29
+ id: string;
30
+ description?: string;
31
+ outputFile: string;
32
+ exampleFile: string;
33
+ entries: EnvTargetEntry[];
34
+ }
35
+ export interface LegacyEnvManifest {
36
+ sources?: string[];
37
+ rootExampleFile?: string;
38
+ variables: Record<string, EnvVariableConfig>;
39
+ targets: Record<string, EnvTargetConfig>;
40
+ }
41
+ export interface SpecEnvManifest {
42
+ sources?: string[];
43
+ rootExampleFile?: string;
44
+ variables: EnvVariableDefinition[];
45
+ targets: EnvTargetDefinition[];
46
+ }
47
+ export type EnvManifest = LegacyEnvManifest | SpecEnvManifest;
48
+ export interface WorkspaceEnvConfig {
49
+ enabled?: boolean;
50
+ manifestPath?: string;
51
+ }
52
+ export interface SyncSummary {
53
+ sourceFiles: string[];
54
+ touchedFiles: string[];
55
+ changedFiles: string[];
56
+ unchangedFiles: string[];
57
+ missingRequired: string[];
58
+ }
59
+ export interface ValidationResult {
60
+ ok: boolean;
61
+ sourceFiles: string[];
62
+ readyTargets: string[];
63
+ missingRequiredByTarget: Record<string, string[]>;
64
+ unknownRootKeys: string[];
65
+ unusedRootKeys: string[];
66
+ }
67
+ export declare function parseEnvContent(content: string): Record<string, string>;
68
+ export declare function loadRootEnv(rootDir: string, sources: string[]): Promise<Record<string, string>>;
69
+ export declare function resolveManifestPath(rootDir: string, configuredPath?: string): string;
70
+ export declare function loadManifest(manifestPath: string): Promise<EnvManifest>;
71
+ export declare function syncWorkspaceEnv(rootDir: string, manifest: EnvManifest, options?: {
72
+ targets?: string[];
73
+ checkOnly?: boolean;
74
+ }): Promise<SyncSummary>;
75
+ export declare function validateWorkspaceEnv(rootDir: string, manifest: EnvManifest, options?: {
76
+ targets?: string[];
77
+ }): Promise<ValidationResult>;
78
+ declare const extension: VersioningExtension;
79
+ export default extension;
80
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,516 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseEnvContent = parseEnvContent;
37
+ exports.loadRootEnv = loadRootEnv;
38
+ exports.resolveManifestPath = resolveManifestPath;
39
+ exports.loadManifest = loadManifest;
40
+ exports.syncWorkspaceEnv = syncWorkspaceEnv;
41
+ exports.validateWorkspaceEnv = validateWorkspaceEnv;
42
+ const fs = __importStar(require("fs-extra"));
43
+ const path = __importStar(require("path"));
44
+ const DEFAULT_MANIFEST_CANDIDATES = [
45
+ 'config/env/manifest.ts',
46
+ 'config/env/manifest.cjs',
47
+ 'config/env/manifest.js',
48
+ 'config/env/manifest.json'
49
+ ];
50
+ const GENERATED_HEADER = 'Generated by @edcalderon/versioning workspace-env. Do not edit directly.';
51
+ function parseEnvContent(content) {
52
+ const values = {};
53
+ const lines = content.split(/\r?\n/);
54
+ for (const line of lines) {
55
+ const trimmed = line.trim().replace(/^export\s+/, '');
56
+ if (!trimmed || trimmed.startsWith('#'))
57
+ continue;
58
+ const eqIndex = trimmed.indexOf('=');
59
+ if (eqIndex <= 0)
60
+ continue;
61
+ const key = trimmed.slice(0, eqIndex).trim();
62
+ let value = trimmed.slice(eqIndex + 1).trim();
63
+ if ((value.startsWith('"') && value.endsWith('"')) ||
64
+ (value.startsWith("'") && value.endsWith("'"))) {
65
+ value = value.slice(1, -1);
66
+ }
67
+ values[key] = value;
68
+ }
69
+ return values;
70
+ }
71
+ async function loadRootEnv(rootDir, sources) {
72
+ const merged = {};
73
+ for (const source of sources) {
74
+ const sourcePath = path.resolve(rootDir, source);
75
+ if (!(await fs.pathExists(sourcePath)))
76
+ continue;
77
+ const content = await fs.readFile(sourcePath, 'utf-8');
78
+ const parsed = parseEnvContent(content);
79
+ Object.assign(merged, parsed);
80
+ }
81
+ return merged;
82
+ }
83
+ function resolveManifestPath(rootDir, configuredPath) {
84
+ if (configuredPath) {
85
+ return path.resolve(rootDir, configuredPath);
86
+ }
87
+ for (const candidate of DEFAULT_MANIFEST_CANDIDATES) {
88
+ const candidatePath = path.resolve(rootDir, candidate);
89
+ if (fs.existsSync(candidatePath)) {
90
+ return candidatePath;
91
+ }
92
+ }
93
+ throw new Error(`Workspace env manifest not found. Searched: ${DEFAULT_MANIFEST_CANDIDATES.join(', ')}. ` +
94
+ 'Set extensionConfig["workspace-env"].manifestPath in versioning.config.json to customize the location.');
95
+ }
96
+ async function loadManifest(manifestPath) {
97
+ const ext = path.extname(manifestPath).toLowerCase();
98
+ if (ext === '.json') {
99
+ return await fs.readJson(manifestPath);
100
+ }
101
+ try {
102
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
103
+ const required = require(manifestPath);
104
+ return required.default || required;
105
+ }
106
+ catch (error) {
107
+ if (ext === '.ts') {
108
+ throw new Error(`Unable to load TypeScript manifest at ${manifestPath}. Use .cjs, .js, or .json, or ensure your runtime can require TypeScript files.`);
109
+ }
110
+ throw error;
111
+ }
112
+ }
113
+ function isSpecManifest(manifest) {
114
+ return Array.isArray(manifest.variables);
115
+ }
116
+ function normalizeManifest(manifest) {
117
+ const sources = sortedUnique((manifest.sources || ['.env', '.env.local']).map((source) => source.trim()).filter(Boolean));
118
+ const rootExampleFile = 'rootExampleFile' in manifest && manifest.rootExampleFile
119
+ ? manifest.rootExampleFile
120
+ : '.env.example';
121
+ if (isSpecManifest(manifest)) {
122
+ const variables = {};
123
+ for (const variable of manifest.variables) {
124
+ variables[variable.key] = {
125
+ key: variable.key,
126
+ aliases: sortedUnique(variable.aliases || []),
127
+ description: variable.description,
128
+ example: variable.example,
129
+ secret: variable.secret
130
+ };
131
+ }
132
+ const targets = {};
133
+ for (const target of manifest.targets) {
134
+ const entries = target.entries.map((entry) => {
135
+ if (!variables[entry.source]) {
136
+ throw new Error(`workspace-env target "${target.id}" references unknown variable "${entry.source}"`);
137
+ }
138
+ return {
139
+ source: entry.source,
140
+ target: entry.target || entry.source,
141
+ required: entry.required === true
142
+ };
143
+ });
144
+ targets[target.id] = {
145
+ id: target.id,
146
+ description: target.description,
147
+ outputFile: target.outputFile,
148
+ exampleFile: target.exampleFile,
149
+ entries: sortEntries(entries)
150
+ };
151
+ }
152
+ return {
153
+ sources,
154
+ rootExampleFile,
155
+ variables,
156
+ targets: sortTargets(targets)
157
+ };
158
+ }
159
+ const variables = {};
160
+ for (const key of Object.keys(manifest.variables || {}).sort()) {
161
+ const variable = manifest.variables[key];
162
+ variables[key] = {
163
+ key,
164
+ aliases: sortedUnique(variable.aliases || []),
165
+ description: variable.description,
166
+ example: variable.example,
167
+ secret: variable.secret
168
+ };
169
+ }
170
+ const targets = {};
171
+ for (const targetId of Object.keys(manifest.targets || {}).sort()) {
172
+ const target = manifest.targets[targetId];
173
+ const entries = [];
174
+ for (const key of Object.keys(manifest.variables || {}).sort()) {
175
+ const variable = manifest.variables[key];
176
+ const mappedKey = variable.targets?.[targetId];
177
+ if (!mappedKey)
178
+ continue;
179
+ entries.push({
180
+ source: key,
181
+ target: mappedKey,
182
+ required: variable.required === true
183
+ });
184
+ }
185
+ targets[targetId] = {
186
+ id: targetId,
187
+ description: target.description,
188
+ outputFile: path.join(target.path, target.envFile || '.env.local'),
189
+ exampleFile: path.join(target.path, target.exampleFile || '.env.example'),
190
+ entries: sortEntries(entries)
191
+ };
192
+ }
193
+ return {
194
+ sources,
195
+ rootExampleFile,
196
+ variables,
197
+ targets: sortTargets(targets)
198
+ };
199
+ }
200
+ function pickCanonicalValue(canonicalKey, variable, rootValues) {
201
+ if (rootValues[canonicalKey] !== undefined)
202
+ return rootValues[canonicalKey];
203
+ for (const alias of variable.aliases || []) {
204
+ if (rootValues[alias] !== undefined)
205
+ return rootValues[alias];
206
+ }
207
+ return undefined;
208
+ }
209
+ function formatEnvFile(entries, fileLabel) {
210
+ const lines = [`# ${GENERATED_HEADER}`, `# ${fileLabel}`, ''];
211
+ for (const entry of entries) {
212
+ if (entry.description) {
213
+ lines.push(`# ${entry.description}`);
214
+ }
215
+ lines.push(`${entry.key}=${entry.value}`);
216
+ }
217
+ return `${lines.join('\n')}\n`;
218
+ }
219
+ async function writeIfChanged(filePath, content, checkOnly = false) {
220
+ const exists = await fs.pathExists(filePath);
221
+ if (exists) {
222
+ const current = await fs.readFile(filePath, 'utf-8');
223
+ if (current === content)
224
+ return false;
225
+ }
226
+ if (checkOnly) {
227
+ return true;
228
+ }
229
+ await fs.ensureDir(path.dirname(filePath));
230
+ await fs.writeFile(filePath, content, 'utf-8');
231
+ return true;
232
+ }
233
+ function getRelevantTargets(manifest, targetFilter) {
234
+ const allTargets = Object.keys(manifest.targets || {}).sort();
235
+ if (!targetFilter || targetFilter.length === 0)
236
+ return allTargets;
237
+ const selected = new Set(targetFilter);
238
+ return allTargets.filter((target) => selected.has(target));
239
+ }
240
+ function sortedUnique(values) {
241
+ return Array.from(new Set(values)).sort();
242
+ }
243
+ function sortEntries(entries) {
244
+ return [...entries].sort((left, right) => left.target.localeCompare(right.target));
245
+ }
246
+ function sortTargets(targets) {
247
+ return Object.fromEntries(Object.entries(targets).sort(([left], [right]) => left.localeCompare(right)));
248
+ }
249
+ function collectTargetOption(value, previous = []) {
250
+ const nextValues = value
251
+ .split(',')
252
+ .map((target) => target.trim())
253
+ .filter(Boolean);
254
+ return [...previous, ...nextValues];
255
+ }
256
+ function relativeSort(rootDir, filePaths) {
257
+ return [...filePaths].map((filePath) => path.relative(rootDir, filePath)).sort();
258
+ }
259
+ function createAliasLookup(manifest) {
260
+ const aliases = new Map();
261
+ for (const variable of Object.values(manifest.variables)) {
262
+ for (const alias of variable.aliases) {
263
+ aliases.set(alias, variable.key);
264
+ }
265
+ }
266
+ return aliases;
267
+ }
268
+ async function analyzeWorkspaceEnv(rootDir, manifestInput, options) {
269
+ const manifest = normalizeManifest(manifestInput);
270
+ const rootValues = await loadRootEnv(rootDir, manifest.sources);
271
+ const sourceFiles = relativeSort(rootDir, (await Promise.all(manifest.sources.map(async (source) => {
272
+ const sourcePath = path.resolve(rootDir, source);
273
+ return (await fs.pathExists(sourcePath)) ? sourcePath : null;
274
+ }))).filter((sourcePath) => sourcePath !== null));
275
+ const selectedTargets = getRelevantTargets(manifest, sortedUnique(options?.targets || []));
276
+ const aliasLookup = createAliasLookup(manifest);
277
+ const usedCanonicalKeys = new Set();
278
+ const missingRequiredByTarget = {};
279
+ const readyTargets = [];
280
+ for (const targetName of selectedTargets) {
281
+ const target = manifest.targets[targetName];
282
+ const missing = [];
283
+ for (const entry of target.entries) {
284
+ usedCanonicalKeys.add(entry.source);
285
+ const variable = manifest.variables[entry.source];
286
+ const value = pickCanonicalValue(entry.source, variable, rootValues);
287
+ if (entry.required && value === undefined) {
288
+ missing.push(entry.source);
289
+ }
290
+ }
291
+ missingRequiredByTarget[targetName] = missing;
292
+ if (missing.length === 0) {
293
+ readyTargets.push(targetName);
294
+ }
295
+ }
296
+ const unknownRootKeys = [];
297
+ const unusedRootKeys = [];
298
+ for (const rootKey of Object.keys(rootValues).sort()) {
299
+ const canonicalKey = manifest.variables[rootKey] ? rootKey : aliasLookup.get(rootKey);
300
+ if (!canonicalKey) {
301
+ unknownRootKeys.push(rootKey);
302
+ continue;
303
+ }
304
+ if (!usedCanonicalKeys.has(canonicalKey)) {
305
+ unusedRootKeys.push(rootKey);
306
+ }
307
+ }
308
+ return {
309
+ manifest,
310
+ rootValues,
311
+ sourceFiles,
312
+ selectedTargets,
313
+ readyTargets,
314
+ missingRequiredByTarget,
315
+ unknownRootKeys,
316
+ unusedRootKeys
317
+ };
318
+ }
319
+ async function syncWorkspaceEnv(rootDir, manifest, options) {
320
+ const analysis = await analyzeWorkspaceEnv(rootDir, manifest, options);
321
+ const changedFiles = [];
322
+ const unchangedFiles = [];
323
+ const missingRequired = [];
324
+ for (const [targetName, missing] of Object.entries(analysis.missingRequiredByTarget)) {
325
+ missingRequired.push(...missing.map((key) => `${targetName}:${key}`));
326
+ }
327
+ const targetNames = analysis.selectedTargets;
328
+ for (const targetName of targetNames) {
329
+ const target = analysis.manifest.targets[targetName];
330
+ const targetEnvEntries = [];
331
+ const targetExampleEntries = [];
332
+ for (const entry of target.entries) {
333
+ const variable = analysis.manifest.variables[entry.source];
334
+ const value = pickCanonicalValue(entry.source, variable, analysis.rootValues);
335
+ if (value !== undefined) {
336
+ targetEnvEntries.push({ key: entry.target, value, description: variable.description });
337
+ }
338
+ const exampleValue = variable.example ?? (variable.secret ? '' : (value ?? ''));
339
+ targetExampleEntries.push({ key: entry.target, value: exampleValue, description: variable.description });
340
+ }
341
+ const envPath = path.resolve(rootDir, target.outputFile);
342
+ const examplePath = path.resolve(rootDir, target.exampleFile);
343
+ const envChanged = await writeIfChanged(envPath, formatEnvFile(targetEnvEntries, `Runtime env for target ${targetName}`), options?.checkOnly === true);
344
+ const exampleChanged = await writeIfChanged(examplePath, formatEnvFile(targetExampleEntries, `Example env for target ${targetName}`), options?.checkOnly === true);
345
+ (envChanged ? changedFiles : unchangedFiles).push(path.relative(rootDir, envPath));
346
+ (exampleChanged ? changedFiles : unchangedFiles).push(path.relative(rootDir, examplePath));
347
+ }
348
+ const rootExampleEntries = Object.keys(analysis.manifest.variables).sort().map((canonical) => {
349
+ const variable = analysis.manifest.variables[canonical];
350
+ const value = pickCanonicalValue(canonical, variable, analysis.rootValues);
351
+ const exampleValue = variable.example ?? (variable.secret ? '' : (value ?? ''));
352
+ return {
353
+ key: canonical,
354
+ value: exampleValue,
355
+ description: variable.description
356
+ };
357
+ });
358
+ const rootExamplePath = path.resolve(rootDir, analysis.manifest.rootExampleFile);
359
+ const rootChanged = await writeIfChanged(rootExamplePath, formatEnvFile(rootExampleEntries, 'Canonical root env example'), options?.checkOnly === true);
360
+ (rootChanged ? changedFiles : unchangedFiles).push(path.relative(rootDir, rootExamplePath));
361
+ changedFiles.sort();
362
+ unchangedFiles.sort();
363
+ missingRequired.sort();
364
+ return {
365
+ sourceFiles: analysis.sourceFiles,
366
+ touchedFiles: [...changedFiles, ...unchangedFiles].sort(),
367
+ changedFiles,
368
+ unchangedFiles,
369
+ missingRequired
370
+ };
371
+ }
372
+ async function validateWorkspaceEnv(rootDir, manifest, options) {
373
+ const analysis = await analyzeWorkspaceEnv(rootDir, manifest, options);
374
+ const hasMissing = Object.values(analysis.missingRequiredByTarget).some((missing) => missing.length > 0);
375
+ return {
376
+ ok: !hasMissing,
377
+ sourceFiles: analysis.sourceFiles,
378
+ readyTargets: analysis.readyTargets,
379
+ missingRequiredByTarget: analysis.missingRequiredByTarget,
380
+ unknownRootKeys: analysis.unknownRootKeys,
381
+ unusedRootKeys: analysis.unusedRootKeys
382
+ };
383
+ }
384
+ function printHumanSyncSummary(summary) {
385
+ console.log(`Sources: ${summary.sourceFiles.length > 0 ? summary.sourceFiles.join(', ') : '(none found)'}`);
386
+ console.log(`Touched files: ${summary.touchedFiles.length}`);
387
+ for (const filePath of summary.changedFiles) {
388
+ console.log(` changed ${filePath}`);
389
+ }
390
+ for (const filePath of summary.unchangedFiles) {
391
+ console.log(` unchanged ${filePath}`);
392
+ }
393
+ if (summary.missingRequired.length > 0) {
394
+ console.log('Missing required canonical variables:');
395
+ for (const missing of summary.missingRequired) {
396
+ console.log(` - ${missing}`);
397
+ }
398
+ }
399
+ }
400
+ function printHumanValidation(result) {
401
+ console.log(`Sources: ${result.sourceFiles.length > 0 ? result.sourceFiles.join(', ') : '(none found)'}`);
402
+ for (const target of result.readyTargets) {
403
+ console.log(`โœ… ${target} ready`);
404
+ }
405
+ for (const [target, missing] of Object.entries(result.missingRequiredByTarget)) {
406
+ if (missing.length === 0)
407
+ continue;
408
+ console.log(`โŒ ${target} missing required: ${missing.join(', ')}`);
409
+ }
410
+ if (result.unknownRootKeys.length > 0) {
411
+ console.log(`โš ๏ธ Unknown root keys: ${result.unknownRootKeys.join(', ')}`);
412
+ }
413
+ if (result.unusedRootKeys.length > 0) {
414
+ console.log(`โ„น๏ธ Unused root keys: ${result.unusedRootKeys.join(', ')}`);
415
+ }
416
+ }
417
+ const extension = {
418
+ name: 'workspace-env',
419
+ description: 'Workspace environment manifest management for monorepos',
420
+ version: '1.0.0',
421
+ register: async (program, config) => {
422
+ const extensionConfig = config?.extensionConfig?.['workspace-env'];
423
+ if (extensionConfig?.enabled === false) {
424
+ return;
425
+ }
426
+ const env = program
427
+ .command('env')
428
+ .description('Workspace env manifest tools (sync, doctor, validate)');
429
+ env
430
+ .command('sync')
431
+ .description('Generate target env files from canonical manifest')
432
+ .option('-t, --target <id>', 'Target to process; repeat or use comma-separated values', collectTargetOption, [])
433
+ .option('--check', 'Exit with non-zero code if generated files would change', false)
434
+ .option('--json', 'Output machine-readable JSON summary', false)
435
+ .action(async (options) => {
436
+ const rootDir = process.cwd();
437
+ const manifestPath = resolveManifestPath(rootDir, extensionConfig?.manifestPath);
438
+ const manifest = await loadManifest(manifestPath);
439
+ const targets = sortedUnique(options.target || []);
440
+ const summary = await syncWorkspaceEnv(rootDir, manifest, {
441
+ targets,
442
+ checkOnly: options.check === true
443
+ });
444
+ if (options.json) {
445
+ console.log(JSON.stringify(summary, null, 2));
446
+ }
447
+ else {
448
+ printHumanSyncSummary(summary);
449
+ console.log(`โœ… env sync complete (${summary.changedFiles.length} changed, ${summary.unchangedFiles.length} unchanged)`);
450
+ }
451
+ if (options.check === true && summary.changedFiles.length > 0) {
452
+ process.exit(1);
453
+ }
454
+ });
455
+ env
456
+ .command('doctor')
457
+ .description('Check missing required vars and unknown root vars')
458
+ .option('-t, --target <id>', 'Target to inspect; repeat or use comma-separated values', collectTargetOption, [])
459
+ .option('--fail-on-missing', 'Exit with non-zero code if required vars are missing', false)
460
+ .option('--json', 'Output machine-readable JSON report', false)
461
+ .action(async (options) => {
462
+ const rootDir = process.cwd();
463
+ const manifestPath = resolveManifestPath(rootDir, extensionConfig?.manifestPath);
464
+ const manifest = await loadManifest(manifestPath);
465
+ const targets = sortedUnique(options.target || []);
466
+ const result = await validateWorkspaceEnv(rootDir, manifest, { targets });
467
+ if (options.json) {
468
+ console.log(JSON.stringify(result, null, 2));
469
+ }
470
+ else {
471
+ printHumanValidation(result);
472
+ if (result.readyTargets.length === 0 && result.unknownRootKeys.length === 0 && result.unusedRootKeys.length === 0) {
473
+ console.log('โš ๏ธ No ready targets found.');
474
+ }
475
+ }
476
+ const hasMissing = Object.values(result.missingRequiredByTarget).some((missing) => missing.length > 0);
477
+ if (!hasMissing && !options.json) {
478
+ console.log('โœ… env doctor: no missing required variables');
479
+ }
480
+ if (hasMissing && options.failOnMissing) {
481
+ process.exit(1);
482
+ }
483
+ });
484
+ env
485
+ .command('validate')
486
+ .description('Validate env mapping (CI-friendly, fails on missing required vars)')
487
+ .option('-t, --target <id>', 'Target to validate; repeat or use comma-separated values', collectTargetOption, [])
488
+ .option('--all', 'Validate all targets explicitly', false)
489
+ .option('--strict-unused', 'Fail if unknown or unused root keys are present', false)
490
+ .option('--json', 'Output machine-readable JSON report', false)
491
+ .action(async (options) => {
492
+ const rootDir = process.cwd();
493
+ const manifestPath = resolveManifestPath(rootDir, extensionConfig?.manifestPath);
494
+ const manifest = await loadManifest(manifestPath);
495
+ const targets = options.all === true ? undefined : sortedUnique(options.target || []);
496
+ const result = await validateWorkspaceEnv(rootDir, manifest, { targets });
497
+ const missing = Object.entries(result.missingRequiredByTarget).filter(([, vars]) => vars.length > 0);
498
+ const hasStrictUnused = options.strictUnused === true && (result.unknownRootKeys.length > 0 || result.unusedRootKeys.length > 0);
499
+ const ok = missing.length === 0 && !hasStrictUnused;
500
+ if (options.json) {
501
+ console.log(JSON.stringify({ ...result, ok }, null, 2));
502
+ }
503
+ else {
504
+ printHumanValidation(result);
505
+ }
506
+ if (!ok) {
507
+ process.exit(1);
508
+ }
509
+ if (!options.json) {
510
+ console.log('โœ… env validate: all required variables are available');
511
+ }
512
+ });
513
+ }
514
+ };
515
+ exports.default = extension;
516
+ //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edcalderon/versioning",
3
- "version": "1.4.7",
3
+ "version": "1.5.1",
4
4
  "description": "A comprehensive versioning and changelog management tool for monorepos",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -13,6 +13,7 @@
13
13
  "test:watch": "jest --watch",
14
14
  "test:coverage": "jest --coverage",
15
15
  "lint": "eslint src/**/*.ts",
16
+ "postinstall": "node scripts/post-install.js",
16
17
  "prepublishOnly": "npm run build && npm run test",
17
18
  "version:patch": "node dist/cli.js patch --no-commit --no-tag",
18
19
  "version:minor": "node dist/cli.js minor --no-commit --no-tag",
@@ -34,6 +35,7 @@
34
35
  ],
35
36
  "author": "Edward",
36
37
  "license": "MIT",
38
+ "homepage": "https://github.com/edcalderon/my-second-brain/tree/main/packages/versioning",
37
39
  "publishConfig": {
38
40
  "access": "public"
39
41
  },
@@ -79,4 +81,4 @@
79
81
  "url": "git+https://github.com/edcalderon/my-second-brain.git",
80
82
  "directory": "packages/versioning"
81
83
  }
82
- }
84
+ }
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-install script: Optionally set up husky if git is available
5
+ * This allows consumers of the versioning package to automatically
6
+ * set up git hooks when git is present.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { execSync } = require('child_process');
12
+
13
+ function isGitRepo() {
14
+ try {
15
+ execSync('git rev-parse --git-dir', { stdio: 'ignore' });
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ function hasHusky() {
23
+ try {
24
+ const rootDir = process.cwd();
25
+ // Check if husky is in node_modules
26
+ const huskyPath = path.join(rootDir, 'node_modules', 'husky');
27
+ return fs.existsSync(huskyPath);
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ function setupHusky() {
34
+ try {
35
+ const rootDir = process.cwd();
36
+ const pkgJsonPath = path.join(rootDir, 'package.json');
37
+
38
+ if (!fs.existsSync(pkgJsonPath)) {
39
+ return; // Not in a package directory
40
+ }
41
+
42
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
43
+
44
+ // Add prepare script if not present
45
+ if (!pkgJson.scripts) {
46
+ pkgJson.scripts = {};
47
+ }
48
+
49
+ if (!pkgJson.scripts.prepare) {
50
+ pkgJson.scripts.prepare = 'husky';
51
+ fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n');
52
+ }
53
+
54
+ // Try to initialize husky
55
+ execSync('npx husky', { stdio: 'ignore' });
56
+ } catch {
57
+ // Silently fail - husky setup is optional
58
+ }
59
+ }
60
+
61
+ // Main logic
62
+ if (isGitRepo() && hasHusky()) {
63
+ setupHusky();
64
+ }