@edcalderon/versioning 1.4.6 โ 1.5.0
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 +24 -0
- package/README.md +112 -9
- package/dist/extensions/workspace-env/index.d.ts +80 -0
- package/dist/extensions/workspace-env/index.js +516 -0
- package/dist/extensions/workspace-scripts/index.d.ts +47 -0
- package/dist/extensions/workspace-scripts/index.js +507 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.5.0](https://github.com/edcalderon/my-second-brain/tree/main/packages/versioning) (2026-03-19)
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- โจ New `workspace-env` extension (v1.0.0)
|
|
12
|
+
- `versioning env sync` to generate per-target `.env.local` and `.env.example` files from one canonical manifest
|
|
13
|
+
- `versioning env doctor` to report missing required variables and unknown root env keys
|
|
14
|
+
- `versioning env validate` for CI-friendly required variable validation with non-zero exit on missing vars
|
|
15
|
+
- Supports manifest sources, aliases, canonical variable metadata, and target key mapping
|
|
16
|
+
- ๐งช Added unit coverage for env parsing, sync generation, validation logic, and command registration
|
|
17
|
+
|
|
18
|
+
## [1.4.7](https://github.com/edcalderon/my-second-brain/tree/main/packages/versioning) (2026-03-16)
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- โจ New `workspace-scripts` extension (v1.0.0)
|
|
22
|
+
- `versioning scripts sync` โ auto-generate `dev:all`, `build:all`, and per-app scripts in root package.json
|
|
23
|
+
- `versioning scripts list` โ display current workspace script configuration
|
|
24
|
+
- `versioning scripts detect` โ find new workspace apps not yet tracked in config
|
|
25
|
+
- `versioning scripts preview` โ preview generated scripts without writing
|
|
26
|
+
- ๐ `postSync` hook auto-detects new apps added to pnpm-workspace.yaml
|
|
27
|
+
- โ๏ธ Config-driven via `extensionConfig.workspace-scripts` in versioning.config.json
|
|
28
|
+
- ๐ Runner support: `concurrently` (default) or `turbo`
|
|
29
|
+
- ๐ฆ Managed script tracking: safely adds/updates/removes only scripts it owns
|
|
30
|
+
- ๐งช Comprehensive test suite for workspace-scripts extension
|
|
31
|
+
|
|
8
32
|
## [1.4.6](https://github.com/edcalderon/my-second-brain/tree/main/packages/versioning) (2026-03-01)
|
|
9
33
|
|
|
10
34
|
### Added
|
package/README.md
CHANGED
|
@@ -8,17 +8,15 @@ A comprehensive versioning and changelog management tool designed for monorepos
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
## ๐ Latest Changes (v1.
|
|
11
|
+
## ๐ Latest Changes (v1.5.0)
|
|
12
12
|
|
|
13
13
|
### Added
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- ๐ Fixed README updater picking wrong version from malformed/misordered CHANGELOGs
|
|
21
|
-
- ๐ Fixed `@ed/auth` โ `@edcalderon/auth` import in `apps/dashboard`
|
|
14
|
+
- โจ New `workspace-env` extension (v1.0.0)
|
|
15
|
+
- `versioning env sync` to generate per-target `.env.local` and `.env.example` files from one canonical manifest
|
|
16
|
+
- `versioning env doctor` to report missing required variables and unknown root env keys
|
|
17
|
+
- `versioning env validate` for CI-friendly required variable validation with non-zero exit on missing vars
|
|
18
|
+
- Supports manifest sources, aliases, canonical variable metadata, and target key mapping
|
|
19
|
+
- ๐งช Added unit coverage for env parsing, sync generation, validation logic, and command registration
|
|
22
20
|
|
|
23
21
|
For full version history, see [CHANGELOG.md](./CHANGELOG.md) and [GitHub releases](https://github.com/edcalderon/my-second-brain/releases)
|
|
24
22
|
|
|
@@ -112,6 +110,111 @@ Extensions can hook into the versioning lifecycle:
|
|
|
112
110
|
|
|
113
111
|
### Built-in Extensions
|
|
114
112
|
|
|
113
|
+
#### Workspace Env Extension
|
|
114
|
+
|
|
115
|
+
Adds first-class workspace environment orchestration for monorepos:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
versioning env sync
|
|
119
|
+
versioning env doctor
|
|
120
|
+
versioning env validate --target landing
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Key capabilities:
|
|
124
|
+
- Canonical root env sources: `.env` then `.env.local`
|
|
125
|
+
- Alias resolution for legacy variable names
|
|
126
|
+
- Minimal per-target runtime env generation
|
|
127
|
+
- Root and per-target example file generation from one manifest
|
|
128
|
+
- Deterministic output with generated headers and no rewrites when content is unchanged
|
|
129
|
+
- CI-friendly validation and dry-run sync checks
|
|
130
|
+
|
|
131
|
+
Manifest default lookup order:
|
|
132
|
+
- `config/env/manifest.ts`
|
|
133
|
+
- `config/env/manifest.cjs`
|
|
134
|
+
- `config/env/manifest.js`
|
|
135
|
+
- `config/env/manifest.json`
|
|
136
|
+
|
|
137
|
+
Supported manifest styles:
|
|
138
|
+
- Legacy map-based manifest:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
{
|
|
142
|
+
sources: ['.env', '.env.local'],
|
|
143
|
+
variables: {
|
|
144
|
+
SUPABASE_URL: {
|
|
145
|
+
description: 'Supabase URL',
|
|
146
|
+
required: true,
|
|
147
|
+
targets: {
|
|
148
|
+
landing: 'NEXT_PUBLIC_SUPABASE_URL'
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
targets: {
|
|
153
|
+
landing: {
|
|
154
|
+
path: 'apps/landing'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- Spec-style manifest:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
{
|
|
164
|
+
rootExampleFile: '.env.example',
|
|
165
|
+
variables: [
|
|
166
|
+
{
|
|
167
|
+
key: 'SUPABASE_URL',
|
|
168
|
+
description: 'Supabase URL'
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
key: 'OPENAI_API_KEY',
|
|
172
|
+
aliases: ['LEGACY_OPENAI_KEY'],
|
|
173
|
+
secret: true
|
|
174
|
+
}
|
|
175
|
+
],
|
|
176
|
+
targets: [
|
|
177
|
+
{
|
|
178
|
+
id: 'landing',
|
|
179
|
+
outputFile: 'apps/landing/.env.local',
|
|
180
|
+
exampleFile: 'apps/landing/.env.example',
|
|
181
|
+
entries: [
|
|
182
|
+
{
|
|
183
|
+
source: 'SUPABASE_URL',
|
|
184
|
+
target: 'NEXT_PUBLIC_SUPABASE_URL',
|
|
185
|
+
required: true
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
source: 'OPENAI_API_KEY'
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
CLI options:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
versioning env sync --target landing --check
|
|
200
|
+
versioning env sync --json
|
|
201
|
+
versioning env doctor --target landing --json
|
|
202
|
+
versioning env validate --all --strict-unused
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Basic config (`versioning.config.json`):
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"extensionConfig": {
|
|
210
|
+
"workspace-env": {
|
|
211
|
+
"enabled": true,
|
|
212
|
+
"manifestPath": "config/env/manifest.cjs"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
115
218
|
#### Lifecycle Hooks Extension
|
|
116
219
|
|
|
117
220
|
Demonstrates all available hooks with example business logic:
|
|
@@ -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
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { VersioningExtension } from '../../extensions';
|
|
2
|
+
export interface WorkspaceAppConfig {
|
|
3
|
+
/** The pnpm filter-compatible package name (auto-resolved from app's package.json if omitted) */
|
|
4
|
+
filter?: string;
|
|
5
|
+
/** The npm script to run for dev mode. Default: "dev" */
|
|
6
|
+
command?: string;
|
|
7
|
+
/** Extra args appended to the dev command (e.g. "-p 3000" or "--port 3001") */
|
|
8
|
+
args?: string;
|
|
9
|
+
/** The npm script to run for build. Default: "build" */
|
|
10
|
+
buildCommand?: string;
|
|
11
|
+
/** Extra args appended to the build command */
|
|
12
|
+
buildArgs?: string;
|
|
13
|
+
/** Optional port number (used for display/documentation only) */
|
|
14
|
+
port?: number;
|
|
15
|
+
/** Whether to include this app in dev:all / build:all. Default: true */
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface WorkspaceScriptsConfig {
|
|
19
|
+
/** Master switch. Default: true */
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
/** Apps included in concurrent dev/build scripts, keyed by workspace-relative path */
|
|
22
|
+
apps: Record<string, WorkspaceAppConfig>;
|
|
23
|
+
/** Generate individual dev:<app> scripts for each app. Default: true */
|
|
24
|
+
individualScripts?: boolean;
|
|
25
|
+
/** Generate build:all and individual build:<app> scripts. Default: true */
|
|
26
|
+
buildScripts?: boolean;
|
|
27
|
+
/** The concurrency runner: "concurrently" or "turbo". Default: "concurrently" */
|
|
28
|
+
runner?: 'concurrently' | 'turbo';
|
|
29
|
+
/** Auto-detect new apps in the workspace and prompt. Default: true */
|
|
30
|
+
autoDetect?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface GeneratedScripts {
|
|
33
|
+
/** The full map of script-name โ command to write into root package.json */
|
|
34
|
+
scripts: Record<string, string>;
|
|
35
|
+
/** Human-readable summary of what was generated */
|
|
36
|
+
summary: string[];
|
|
37
|
+
}
|
|
38
|
+
export declare function generateWorkspaceScripts(rootDir: string, wsCfg: WorkspaceScriptsConfig): Promise<GeneratedScripts>;
|
|
39
|
+
export declare function syncScriptsToPackageJson(rootDir: string, generated: GeneratedScripts, dryRun?: boolean): Promise<{
|
|
40
|
+
added: string[];
|
|
41
|
+
updated: string[];
|
|
42
|
+
removed: string[];
|
|
43
|
+
}>;
|
|
44
|
+
export declare function detectNewApps(rootDir: string, wsCfg: WorkspaceScriptsConfig): Promise<string[]>;
|
|
45
|
+
declare const extension: VersioningExtension;
|
|
46
|
+
export default extension;
|
|
47
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,507 @@
|
|
|
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.generateWorkspaceScripts = generateWorkspaceScripts;
|
|
37
|
+
exports.syncScriptsToPackageJson = syncScriptsToPackageJson;
|
|
38
|
+
exports.detectNewApps = detectNewApps;
|
|
39
|
+
const fs = __importStar(require("fs-extra"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
42
|
+
// EXTENSION METADATA
|
|
43
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
44
|
+
const EXTENSION_NAME = 'workspace-scripts';
|
|
45
|
+
const EXTENSION_VERSION = '1.0.0';
|
|
46
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
47
|
+
// MANAGED SCRIPT MARKER
|
|
48
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
49
|
+
/**
|
|
50
|
+
* All scripts generated by this extension are prefixed with one of these keys.
|
|
51
|
+
* During sync we only touch scripts whose key starts with a managed prefix.
|
|
52
|
+
*/
|
|
53
|
+
const MANAGED_PREFIXES = ['dev', 'build'];
|
|
54
|
+
// A hidden marker script key so we know which scripts we own.
|
|
55
|
+
const MARKER_KEY = '__workspace_scripts_managed';
|
|
56
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
57
|
+
// HELPERS
|
|
58
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
59
|
+
function loadWorkspaceScriptsConfig(config) {
|
|
60
|
+
const wsCfg = config?.extensionConfig?.['workspace-scripts'];
|
|
61
|
+
if (!wsCfg || wsCfg.enabled === false)
|
|
62
|
+
return null;
|
|
63
|
+
return {
|
|
64
|
+
enabled: true,
|
|
65
|
+
apps: wsCfg.apps || {},
|
|
66
|
+
individualScripts: wsCfg.individualScripts !== false,
|
|
67
|
+
buildScripts: wsCfg.buildScripts !== false,
|
|
68
|
+
runner: wsCfg.runner || 'concurrently',
|
|
69
|
+
autoDetect: wsCfg.autoDetect !== false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Derive a short name from a workspace path like "apps/dashboard" โ "dashboard" */
|
|
73
|
+
function shortName(wsPath) {
|
|
74
|
+
const parts = wsPath.split('/');
|
|
75
|
+
return parts[parts.length - 1];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Read the package name from a workspace app's package.json.
|
|
79
|
+
* Falls back to deriving from path if package.json doesn't exist.
|
|
80
|
+
*/
|
|
81
|
+
async function resolveFilter(rootDir, wsPath, appCfg) {
|
|
82
|
+
if (appCfg.filter)
|
|
83
|
+
return appCfg.filter;
|
|
84
|
+
const pkgPath = path.join(rootDir, wsPath, 'package.json');
|
|
85
|
+
if (await fs.pathExists(pkgPath)) {
|
|
86
|
+
try {
|
|
87
|
+
const pkg = await fs.readJson(pkgPath);
|
|
88
|
+
if (pkg.name)
|
|
89
|
+
return pkg.name;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// fall through
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Fallback: use the directory name
|
|
96
|
+
return shortName(wsPath);
|
|
97
|
+
}
|
|
98
|
+
/** Build a single pnpm --filter command string */
|
|
99
|
+
function buildFilterCmd(filter, command, args) {
|
|
100
|
+
let cmd = `pnpm --filter ${filter} ${command}`;
|
|
101
|
+
if (args)
|
|
102
|
+
cmd += ` ${args}`;
|
|
103
|
+
return cmd;
|
|
104
|
+
}
|
|
105
|
+
async function generateWorkspaceScripts(rootDir, wsCfg) {
|
|
106
|
+
const scripts = {};
|
|
107
|
+
const summary = [];
|
|
108
|
+
const enabledApps = Object.entries(wsCfg.apps).filter(([, cfg]) => cfg.enabled !== false);
|
|
109
|
+
if (enabledApps.length === 0) {
|
|
110
|
+
summary.push('No enabled apps found in workspace-scripts config');
|
|
111
|
+
return { scripts, summary };
|
|
112
|
+
}
|
|
113
|
+
// โโ Dev scripts โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
114
|
+
const devCmds = [];
|
|
115
|
+
for (const [wsPath, appCfg] of enabledApps) {
|
|
116
|
+
const filter = await resolveFilter(rootDir, wsPath, appCfg);
|
|
117
|
+
const cmd = appCfg.command || 'dev';
|
|
118
|
+
const filterCmd = buildFilterCmd(filter, cmd, appCfg.args);
|
|
119
|
+
devCmds.push(filterCmd);
|
|
120
|
+
// Individual dev:<name> script
|
|
121
|
+
if (wsCfg.individualScripts) {
|
|
122
|
+
const name = shortName(wsPath);
|
|
123
|
+
scripts[`dev:${name}`] = filterCmd;
|
|
124
|
+
summary.push(` dev:${name} โ ${filterCmd}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// dev:all โ concurrent execution
|
|
128
|
+
if (devCmds.length > 0) {
|
|
129
|
+
if (wsCfg.runner === 'turbo') {
|
|
130
|
+
scripts['dev:all'] = 'turbo run dev';
|
|
131
|
+
summary.push(` dev:all โ turbo run dev`);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const quoted = devCmds.map((c) => `"${c}"`).join(' ');
|
|
135
|
+
scripts['dev:all'] = `concurrently ${quoted}`;
|
|
136
|
+
summary.push(` dev:all โ concurrently (${devCmds.length} apps)`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Primary dev โ first app (convenience shortcut)
|
|
140
|
+
if (devCmds.length > 0) {
|
|
141
|
+
scripts['dev'] = devCmds[0];
|
|
142
|
+
summary.push(` dev โ ${devCmds[0]}`);
|
|
143
|
+
}
|
|
144
|
+
// โโ Build scripts โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
145
|
+
if (wsCfg.buildScripts) {
|
|
146
|
+
const buildCmds = [];
|
|
147
|
+
for (const [wsPath, appCfg] of enabledApps) {
|
|
148
|
+
const filter = await resolveFilter(rootDir, wsPath, appCfg);
|
|
149
|
+
const cmd = appCfg.buildCommand || 'build';
|
|
150
|
+
const filterCmd = buildFilterCmd(filter, cmd, appCfg.buildArgs);
|
|
151
|
+
buildCmds.push(filterCmd);
|
|
152
|
+
if (wsCfg.individualScripts) {
|
|
153
|
+
const name = shortName(wsPath);
|
|
154
|
+
scripts[`build:${name}`] = filterCmd;
|
|
155
|
+
summary.push(` build:${name} โ ${filterCmd}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// build:all
|
|
159
|
+
if (buildCmds.length > 0) {
|
|
160
|
+
if (wsCfg.runner === 'turbo') {
|
|
161
|
+
scripts['build:all'] = 'turbo run build';
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
scripts['build:all'] = 'pnpm --recursive build';
|
|
165
|
+
}
|
|
166
|
+
summary.push(` build:all โ pnpm --recursive build`);
|
|
167
|
+
}
|
|
168
|
+
// Primary build โ recursive
|
|
169
|
+
scripts['build'] = 'pnpm --recursive build';
|
|
170
|
+
summary.push(` build โ pnpm --recursive build`);
|
|
171
|
+
}
|
|
172
|
+
return { scripts, summary };
|
|
173
|
+
}
|
|
174
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
175
|
+
// CORE: SYNC SCRIPTS INTO PACKAGE.JSON
|
|
176
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
177
|
+
async function syncScriptsToPackageJson(rootDir, generated, dryRun = false) {
|
|
178
|
+
const pkgPath = path.join(rootDir, 'package.json');
|
|
179
|
+
if (!(await fs.pathExists(pkgPath))) {
|
|
180
|
+
throw new Error(`Root package.json not found at ${pkgPath}`);
|
|
181
|
+
}
|
|
182
|
+
const pkg = (await fs.readJson(pkgPath)) || {};
|
|
183
|
+
if (!pkg.scripts)
|
|
184
|
+
pkg.scripts = {};
|
|
185
|
+
const added = [];
|
|
186
|
+
const updated = [];
|
|
187
|
+
const removed = [];
|
|
188
|
+
// Identify currently managed scripts (from a previous sync)
|
|
189
|
+
const previouslyManaged = new Set();
|
|
190
|
+
if (pkg.scripts[MARKER_KEY]) {
|
|
191
|
+
try {
|
|
192
|
+
const managed = JSON.parse(pkg.scripts[MARKER_KEY]);
|
|
193
|
+
managed.forEach((k) => previouslyManaged.add(k));
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// corrupted marker, ignore
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Remove scripts that we previously managed but are no longer generated
|
|
200
|
+
for (const key of previouslyManaged) {
|
|
201
|
+
if (!(key in generated.scripts)) {
|
|
202
|
+
if (pkg.scripts[key] !== undefined) {
|
|
203
|
+
if (!dryRun)
|
|
204
|
+
delete pkg.scripts[key];
|
|
205
|
+
removed.push(key);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Add/update generated scripts
|
|
210
|
+
for (const [key, value] of Object.entries(generated.scripts)) {
|
|
211
|
+
if (pkg.scripts[key] === undefined) {
|
|
212
|
+
added.push(key);
|
|
213
|
+
}
|
|
214
|
+
else if (pkg.scripts[key] !== value) {
|
|
215
|
+
updated.push(key);
|
|
216
|
+
}
|
|
217
|
+
if (!dryRun)
|
|
218
|
+
pkg.scripts[key] = value;
|
|
219
|
+
}
|
|
220
|
+
// Update the marker with currently managed keys
|
|
221
|
+
if (!dryRun) {
|
|
222
|
+
pkg.scripts[MARKER_KEY] = JSON.stringify(Object.keys(generated.scripts));
|
|
223
|
+
}
|
|
224
|
+
if (!dryRun) {
|
|
225
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
226
|
+
}
|
|
227
|
+
return { added, updated, removed };
|
|
228
|
+
}
|
|
229
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
230
|
+
// CORE: DETECT NEW APPS IN WORKSPACE
|
|
231
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
232
|
+
async function detectNewApps(rootDir, wsCfg) {
|
|
233
|
+
const newApps = [];
|
|
234
|
+
// Read pnpm-workspace.yaml to find all workspace patterns
|
|
235
|
+
const workspaceYamlPath = path.join(rootDir, 'pnpm-workspace.yaml');
|
|
236
|
+
if (!(await fs.pathExists(workspaceYamlPath)))
|
|
237
|
+
return newApps;
|
|
238
|
+
const yamlContent = await fs.readFile(workspaceYamlPath, 'utf-8');
|
|
239
|
+
// Simple YAML parsing for pnpm-workspace.yaml (packages: - pattern)
|
|
240
|
+
const patterns = [];
|
|
241
|
+
const lines = yamlContent.split('\n');
|
|
242
|
+
let inPackages = false;
|
|
243
|
+
for (const line of lines) {
|
|
244
|
+
const trimmed = line.trim();
|
|
245
|
+
if (trimmed === 'packages:') {
|
|
246
|
+
inPackages = true;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (inPackages) {
|
|
250
|
+
if (trimmed.startsWith('-')) {
|
|
251
|
+
const pattern = trimmed.replace(/^-\s*['"]?/, '').replace(/['"]?\s*$/, '');
|
|
252
|
+
patterns.push(pattern);
|
|
253
|
+
}
|
|
254
|
+
else if (trimmed && !trimmed.startsWith('#')) {
|
|
255
|
+
inPackages = false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Expand glob patterns (simple: only handles "foo/*" patterns)
|
|
260
|
+
const knownPaths = new Set(Object.keys(wsCfg.apps));
|
|
261
|
+
for (const pattern of patterns) {
|
|
262
|
+
if (pattern.endsWith('/*')) {
|
|
263
|
+
const dir = pattern.slice(0, -2);
|
|
264
|
+
const absDir = path.join(rootDir, dir);
|
|
265
|
+
if (await fs.pathExists(absDir)) {
|
|
266
|
+
const entries = await fs.readdir(absDir);
|
|
267
|
+
for (const entry of entries) {
|
|
268
|
+
const entryPath = `${dir}/${entry}`;
|
|
269
|
+
const fullPath = path.join(absDir, entry);
|
|
270
|
+
const stat = await fs.stat(fullPath);
|
|
271
|
+
if (!stat.isDirectory())
|
|
272
|
+
continue;
|
|
273
|
+
// Check it has a package.json (is a workspace package)
|
|
274
|
+
const hasPkg = await fs.pathExists(path.join(fullPath, 'package.json'));
|
|
275
|
+
if (!hasPkg)
|
|
276
|
+
continue;
|
|
277
|
+
if (!knownPaths.has(entryPath)) {
|
|
278
|
+
newApps.push(entryPath);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return newApps;
|
|
285
|
+
}
|
|
286
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
287
|
+
// EXTENSION REGISTRATION
|
|
288
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
289
|
+
const extension = {
|
|
290
|
+
name: EXTENSION_NAME,
|
|
291
|
+
description: 'Automates dev:all, build:all, and per-app scripts in root package.json',
|
|
292
|
+
version: EXTENSION_VERSION,
|
|
293
|
+
hooks: {
|
|
294
|
+
postSync: async (options) => {
|
|
295
|
+
try {
|
|
296
|
+
const configPath = options?.config ?? 'versioning.config.json';
|
|
297
|
+
if (!(await fs.pathExists(configPath)))
|
|
298
|
+
return;
|
|
299
|
+
const config = await fs.readJson(configPath);
|
|
300
|
+
const wsCfg = loadWorkspaceScriptsConfig(config);
|
|
301
|
+
if (!wsCfg)
|
|
302
|
+
return;
|
|
303
|
+
if (wsCfg.autoDetect) {
|
|
304
|
+
const rootDir = process.cwd();
|
|
305
|
+
const newApps = await detectNewApps(rootDir, wsCfg);
|
|
306
|
+
if (newApps.length > 0) {
|
|
307
|
+
console.log('');
|
|
308
|
+
console.log('๐ workspace-scripts: New apps detected in workspace:');
|
|
309
|
+
for (const app of newApps) {
|
|
310
|
+
console.log(` ๐ฆ ${app}`);
|
|
311
|
+
}
|
|
312
|
+
console.log('');
|
|
313
|
+
console.log(' Run `versioning scripts add` to configure them,');
|
|
314
|
+
console.log(' or add them to versioning.config.json โ extensionConfig โ workspace-scripts โ apps');
|
|
315
|
+
console.log('');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
console.warn('โ ๏ธ workspace-scripts postSync hook failed:', error instanceof Error ? error.message : String(error));
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
register: async (program, config) => {
|
|
325
|
+
const scriptsCmd = program
|
|
326
|
+
.command('scripts')
|
|
327
|
+
.description('Manage dev/build scripts in root package.json (workspace-scripts extension)');
|
|
328
|
+
// โโ versioning scripts sync โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
329
|
+
scriptsCmd
|
|
330
|
+
.command('sync')
|
|
331
|
+
.description('Regenerate dev:all, build:all, and per-app scripts from config')
|
|
332
|
+
.option('--dry-run', 'Preview changes without writing', false)
|
|
333
|
+
.action(async (options) => {
|
|
334
|
+
try {
|
|
335
|
+
const rootDir = process.cwd();
|
|
336
|
+
const wsCfg = loadWorkspaceScriptsConfig(config);
|
|
337
|
+
if (!wsCfg) {
|
|
338
|
+
console.log('โ ๏ธ workspace-scripts is not configured or disabled.');
|
|
339
|
+
console.log(' Add "workspace-scripts" to extensionConfig in versioning.config.json');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
console.log('๐ Generating workspace scripts...\n');
|
|
343
|
+
const generated = await generateWorkspaceScripts(rootDir, wsCfg);
|
|
344
|
+
if (generated.summary.length === 0) {
|
|
345
|
+
console.log('โ ๏ธ No apps configured. Nothing to generate.');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
console.log('๐ Scripts to generate:');
|
|
349
|
+
for (const line of generated.summary) {
|
|
350
|
+
console.log(line);
|
|
351
|
+
}
|
|
352
|
+
console.log('');
|
|
353
|
+
const result = await syncScriptsToPackageJson(rootDir, generated, options.dryRun);
|
|
354
|
+
if (options.dryRun) {
|
|
355
|
+
console.log('๐ Dry run โ no changes written.\n');
|
|
356
|
+
}
|
|
357
|
+
if (result.added.length > 0) {
|
|
358
|
+
console.log(`โ
Added: ${result.added.join(', ')}`);
|
|
359
|
+
}
|
|
360
|
+
if (result.updated.length > 0) {
|
|
361
|
+
console.log(`๐ Updated: ${result.updated.join(', ')}`);
|
|
362
|
+
}
|
|
363
|
+
if (result.removed.length > 0) {
|
|
364
|
+
console.log(`๐๏ธ Removed: ${result.removed.join(', ')}`);
|
|
365
|
+
}
|
|
366
|
+
if (result.added.length === 0 && result.updated.length === 0 && result.removed.length === 0) {
|
|
367
|
+
console.log('โ
Scripts are already up to date.');
|
|
368
|
+
}
|
|
369
|
+
console.log('');
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
console.error('โ Script sync failed:', error instanceof Error ? error.message : String(error));
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
// โโ versioning scripts list โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
377
|
+
scriptsCmd
|
|
378
|
+
.command('list')
|
|
379
|
+
.description('Show current workspace script configuration')
|
|
380
|
+
.action(async () => {
|
|
381
|
+
try {
|
|
382
|
+
const rootDir = process.cwd();
|
|
383
|
+
const wsCfg = loadWorkspaceScriptsConfig(config);
|
|
384
|
+
if (!wsCfg) {
|
|
385
|
+
console.log('โ ๏ธ workspace-scripts is not configured or disabled.');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
console.log('๐ Workspace Scripts Configuration\n');
|
|
389
|
+
console.log(` Runner: ${wsCfg.runner}`);
|
|
390
|
+
console.log(` Individual scripts: ${wsCfg.individualScripts ? 'yes' : 'no'}`);
|
|
391
|
+
console.log(` Build scripts: ${wsCfg.buildScripts ? 'yes' : 'no'}`);
|
|
392
|
+
console.log(` Auto-detect: ${wsCfg.autoDetect ? 'yes' : 'no'}`);
|
|
393
|
+
console.log('');
|
|
394
|
+
const enabledApps = Object.entries(wsCfg.apps).filter(([, cfg]) => cfg.enabled !== false);
|
|
395
|
+
const disabledApps = Object.entries(wsCfg.apps).filter(([, cfg]) => cfg.enabled === false);
|
|
396
|
+
if (enabledApps.length > 0) {
|
|
397
|
+
console.log(' Enabled apps:');
|
|
398
|
+
for (const [wsPath, appCfg] of enabledApps) {
|
|
399
|
+
const filter = await resolveFilter(rootDir, wsPath, appCfg);
|
|
400
|
+
const cmd = appCfg.command || 'dev';
|
|
401
|
+
const portInfo = appCfg.port ? ` (port ${appCfg.port})` : '';
|
|
402
|
+
console.log(` ๐ฆ ${wsPath} โ ${filter} ${cmd}${appCfg.args ? ' ' + appCfg.args : ''}${portInfo}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (disabledApps.length > 0) {
|
|
406
|
+
console.log('\n Disabled apps:');
|
|
407
|
+
for (const [wsPath] of disabledApps) {
|
|
408
|
+
console.log(` โฌ ${wsPath}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Check for new untracked apps
|
|
412
|
+
if (wsCfg.autoDetect) {
|
|
413
|
+
const newApps = await detectNewApps(rootDir, wsCfg);
|
|
414
|
+
if (newApps.length > 0) {
|
|
415
|
+
console.log('\n ๐ New apps detected (not yet configured):');
|
|
416
|
+
for (const app of newApps) {
|
|
417
|
+
console.log(` ๐ ${app}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
console.log('');
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
console.error('โ Failed to list scripts:', error instanceof Error ? error.message : String(error));
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
// โโ versioning scripts detect โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
429
|
+
scriptsCmd
|
|
430
|
+
.command('detect')
|
|
431
|
+
.description('Detect new workspace apps not yet in scripts config')
|
|
432
|
+
.action(async () => {
|
|
433
|
+
try {
|
|
434
|
+
const rootDir = process.cwd();
|
|
435
|
+
const wsCfg = loadWorkspaceScriptsConfig(config);
|
|
436
|
+
if (!wsCfg) {
|
|
437
|
+
console.log('โ ๏ธ workspace-scripts is not configured or disabled.');
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const newApps = await detectNewApps(rootDir, wsCfg);
|
|
441
|
+
if (newApps.length === 0) {
|
|
442
|
+
console.log('โ
All workspace apps are tracked in scripts config.');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
console.log(`๐ Found ${newApps.length} new app(s) not in workspace-scripts config:\n`);
|
|
446
|
+
for (const app of newApps) {
|
|
447
|
+
const pkgPath = path.join(rootDir, app, 'package.json');
|
|
448
|
+
let pkgName = shortName(app);
|
|
449
|
+
if (await fs.pathExists(pkgPath)) {
|
|
450
|
+
try {
|
|
451
|
+
const pkg = await fs.readJson(pkgPath);
|
|
452
|
+
if (pkg.name)
|
|
453
|
+
pkgName = pkg.name;
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
// ignore
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
console.log(` ๐ฆ ${app} (${pkgName})`);
|
|
460
|
+
}
|
|
461
|
+
console.log('\n๐ To add them, update versioning.config.json:');
|
|
462
|
+
console.log(' extensionConfig โ workspace-scripts โ apps\n');
|
|
463
|
+
// Show example config for the first new app
|
|
464
|
+
if (newApps.length > 0) {
|
|
465
|
+
const example = newApps[0];
|
|
466
|
+
console.log(' Example:');
|
|
467
|
+
console.log(` "${example}": { "command": "dev", "args": "-p 3000" }\n`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
console.error('โ Detection failed:', error instanceof Error ? error.message : String(error));
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
// โโ versioning scripts preview โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
476
|
+
scriptsCmd
|
|
477
|
+
.command('preview')
|
|
478
|
+
.description('Preview the scripts that would be generated without writing')
|
|
479
|
+
.action(async () => {
|
|
480
|
+
try {
|
|
481
|
+
const rootDir = process.cwd();
|
|
482
|
+
const wsCfg = loadWorkspaceScriptsConfig(config);
|
|
483
|
+
if (!wsCfg) {
|
|
484
|
+
console.log('โ ๏ธ workspace-scripts is not configured or disabled.');
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const generated = await generateWorkspaceScripts(rootDir, wsCfg);
|
|
488
|
+
if (Object.keys(generated.scripts).length === 0) {
|
|
489
|
+
console.log('โ ๏ธ No scripts would be generated.');
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
console.log('๐ Preview โ scripts that would be written to root package.json:\n');
|
|
493
|
+
const maxKeyLen = Math.max(...Object.keys(generated.scripts).map((k) => k.length));
|
|
494
|
+
for (const [key, value] of Object.entries(generated.scripts)) {
|
|
495
|
+
console.log(` "${key.padEnd(maxKeyLen)}" : "${value}"`);
|
|
496
|
+
}
|
|
497
|
+
console.log('');
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
console.error('โ Preview failed:', error instanceof Error ? error.message : String(error));
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
exports.default = extension;
|
|
507
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edcalderon/versioning",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "A comprehensive versioning and changelog management tool for monorepos",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
],
|
|
35
35
|
"author": "Edward",
|
|
36
36
|
"license": "MIT",
|
|
37
|
+
"homepage": "https://github.com/edcalderon/my-second-brain/tree/main/packages/versioning",
|
|
37
38
|
"publishConfig": {
|
|
38
39
|
"access": "public"
|
|
39
40
|
},
|