@akshxy/envgit 0.3.0 → 0.4.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/bin/envgit.js CHANGED
@@ -37,9 +37,10 @@ program
37
37
  .action(status);
38
38
 
39
39
  program
40
- .command('set <assignments...>')
41
- .description('Set one or more KEY=VALUE pairs (defaults to active env)')
40
+ .command('set [assignments...]')
41
+ .description('Set KEY=VALUE pairs, or load from a file with -f')
42
42
  .option('--env <name>', 'target environment')
43
+ .option('-f, --file <path>', 'read variables from a .env file')
43
44
  .action(set);
44
45
 
45
46
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akshxy/envgit",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Encrypted per-project environment variable manager",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,9 @@
1
+ import { join } from 'path';
2
+ import { existsSync } from 'fs';
1
3
  import { requireProjectRoot, loadKey } from '../keystore.js';
2
4
  import { resolveEnv } from '../config.js';
3
5
  import { readEncEnv, writeEncEnv } from '../enc.js';
6
+ import { readEnvFile } from '../envfile.js';
4
7
  import { getCurrentEnv } from '../state.js';
5
8
  import { ok, fatal, label } from '../ui.js';
6
9
 
@@ -11,15 +14,31 @@ export async function set(assignments, options) {
11
14
 
12
15
  const vars = readEncEnv(projectRoot, envName, key);
13
16
 
14
- for (const assignment of assignments) {
15
- const eqIdx = assignment.indexOf('=');
16
- if (eqIdx === -1) {
17
- fatal(`Invalid assignment '${assignment}'. Expected KEY=VALUE format.`);
17
+ if (options.file) {
18
+ const filePath = join(projectRoot, options.file);
19
+ if (!existsSync(filePath)) {
20
+ fatal(`File not found: ${options.file}`);
21
+ }
22
+ const fileVars = readEnvFile(filePath);
23
+ const entries = Object.entries(fileVars);
24
+ if (entries.length === 0) {
25
+ fatal(`No variables found in ${options.file}`);
26
+ }
27
+ for (const [k, v] of entries) {
28
+ vars[k] = v;
29
+ ok(`Set ${k} in ${label(envName)}`);
30
+ }
31
+ } else {
32
+ for (const assignment of assignments) {
33
+ const eqIdx = assignment.indexOf('=');
34
+ if (eqIdx === -1) {
35
+ fatal(`Invalid assignment '${assignment}'. Expected KEY=VALUE format.`);
36
+ }
37
+ const k = assignment.slice(0, eqIdx).trim();
38
+ const v = assignment.slice(eqIdx + 1);
39
+ vars[k] = v;
40
+ ok(`Set ${k} in ${label(envName)}`);
18
41
  }
19
- const k = assignment.slice(0, eqIdx).trim();
20
- const v = assignment.slice(eqIdx + 1);
21
- vars[k] = v;
22
- ok(`Set ${k} in ${label(envName)}`);
23
42
  }
24
43
 
25
44
  writeEncEnv(projectRoot, envName, key, vars);
package/src/envfile.js CHANGED
@@ -1,5 +1,338 @@
1
1
  import { readFileSync, writeFileSync, existsSync } from 'fs';
2
2
 
3
+ // ─── Section label mappings (prefix → section name) ──────────────────────────
4
+ const SECTION_LABELS = {
5
+ // ── App / Runtime ──────────────────────────────────────────────────────────
6
+ NODE: 'App / Runtime',
7
+ APP: 'App / Config',
8
+ SERVER: 'App / Server',
9
+ SERVICE: 'App / Service',
10
+ WORKER: 'App / Worker',
11
+
12
+ // ── Frontend Frameworks ────────────────────────────────────────────────────
13
+ NEXT: 'Next.js',
14
+ NEXT_PUBLIC: 'Next.js / Public (client-side)',
15
+ NUXT: 'Nuxt',
16
+ NUXT_PUBLIC: 'Nuxt / Public (client-side)',
17
+ VITE: 'Vite',
18
+ VITE_APP: 'Vite / Public (client-side)',
19
+ REACT: 'React',
20
+ REACT_APP: 'React / Public (client-side)',
21
+ GATSBY: 'Gatsby',
22
+ SVELTE: 'SvelteKit',
23
+ PUBLIC: 'Public (client-side)',
24
+
25
+ // ── Databases ──────────────────────────────────────────────────────────────
26
+ DB: 'Database',
27
+ DATABASE: 'Database',
28
+ POSTGRES: 'Database / PostgreSQL',
29
+ POSTGRESQL: 'Database / PostgreSQL',
30
+ PG: 'Database / PostgreSQL',
31
+ PGHOST: 'Database / PostgreSQL',
32
+ PGPORT: 'Database / PostgreSQL',
33
+ PGUSER: 'Database / PostgreSQL',
34
+ PGPASSWORD: 'Database / PostgreSQL',
35
+ PGDATABASE: 'Database / PostgreSQL',
36
+ MYSQL: 'Database / MySQL',
37
+ MARIADB: 'Database / MariaDB',
38
+ MONGO: 'Database / MongoDB',
39
+ MONGODB: 'Database / MongoDB',
40
+ SQLITE: 'Database / SQLite',
41
+ COCKROACH: 'Database / CockroachDB',
42
+ COCKROACHDB: 'Database / CockroachDB',
43
+ NEON: 'Database / Neon',
44
+ PLANETSCALE: 'Database / PlanetScale',
45
+ TURSO: 'Database / Turso',
46
+ SUPABASE: 'Supabase',
47
+ XATA: 'Database / Xata',
48
+ CONVEX: 'Database / Convex',
49
+
50
+ // ── Cache / Queues ─────────────────────────────────────────────────────────
51
+ REDIS: 'Cache / Redis',
52
+ UPSTASH: 'Cache / Upstash Redis',
53
+ MEMCACHED: 'Cache / Memcached',
54
+ KAFKA: 'Queue / Kafka',
55
+ RABBITMQ: 'Queue / RabbitMQ',
56
+ SQS: 'Queue / AWS SQS',
57
+ BULL: 'Queue / Bull',
58
+ INNGEST: 'Queue / Inngest',
59
+ TRIGGER: 'Queue / Trigger.dev',
60
+ QSTASH: 'Queue / QStash',
61
+
62
+ // ── Auth ───────────────────────────────────────────────────────────────────
63
+ AUTH: 'Auth',
64
+ AUTH0: 'Auth / Auth0',
65
+ CLERK: 'Auth / Clerk',
66
+ NEXTAUTH: 'Auth / NextAuth',
67
+ NEXT_AUTH: 'Auth / NextAuth',
68
+ LUCIA: 'Auth / Lucia',
69
+ BETTER_AUTH: 'Auth / Better Auth',
70
+ SUPABASE_AUTH: 'Auth / Supabase Auth',
71
+ FIREBASE_AUTH: 'Auth / Firebase Auth',
72
+ COGNITO: 'Auth / AWS Cognito',
73
+ OKTA: 'Auth / Okta',
74
+ WORKOS: 'Auth / WorkOS',
75
+ PROPELAUTH: 'Auth / PropelAuth',
76
+ STYTCH: 'Auth / Stytch',
77
+ MAGIC: 'Auth / Magic',
78
+ OAUTH: 'Auth / OAuth',
79
+ JWT: 'Auth / JWT',
80
+ SESSION: 'Auth / Session',
81
+ COOKIE: 'Auth / Cookie',
82
+ CSRF: 'Auth / CSRF',
83
+ TOTP: 'Auth / 2FA',
84
+ MFA: 'Auth / 2FA',
85
+
86
+ // ── AWS ────────────────────────────────────────────────────────────────────
87
+ AWS: 'AWS',
88
+ S3: 'AWS / S3',
89
+ EC2: 'AWS / EC2',
90
+ ECS: 'AWS / ECS',
91
+ EKS: 'AWS / EKS',
92
+ LAMBDA: 'AWS / Lambda',
93
+ CLOUDFRONT: 'AWS / CloudFront',
94
+ CLOUDWATCH: 'AWS / CloudWatch',
95
+ COGNITO: 'AWS / Cognito',
96
+ DYNAMODB: 'AWS / DynamoDB',
97
+ SES: 'AWS / SES',
98
+ SNS: 'AWS / SNS',
99
+ SQS: 'AWS / SQS',
100
+ ROUTE53: 'AWS / Route 53',
101
+ ECR: 'AWS / ECR',
102
+ SECRETS: 'AWS / Secrets Manager',
103
+
104
+ // ── Google Cloud ───────────────────────────────────────────────────────────
105
+ GCP: 'Google Cloud',
106
+ GOOGLE: 'Google',
107
+ GOOGLE_CLOUD: 'Google Cloud',
108
+ FIREBASE: 'Firebase',
109
+ FIRESTORE: 'Firebase / Firestore',
110
+ GCS: 'Google Cloud / Storage',
111
+ BIGQUERY: 'Google Cloud / BigQuery',
112
+ PUBSUB: 'Google Cloud / Pub/Sub',
113
+ GCLOUD: 'Google Cloud',
114
+
115
+ // ── Azure ──────────────────────────────────────────────────────────────────
116
+ AZURE: 'Azure',
117
+ AZURE_AD: 'Azure / Active Directory',
118
+ COSMOS: 'Azure / Cosmos DB',
119
+ COSMOSDB: 'Azure / Cosmos DB',
120
+ BLOB: 'Azure / Blob Storage',
121
+
122
+ // ── AI / LLMs ──────────────────────────────────────────────────────────────
123
+ OPENAI: 'AI / OpenAI',
124
+ ANTHROPIC: 'AI / Anthropic',
125
+ GEMINI: 'AI / Google Gemini',
126
+ COHERE: 'AI / Cohere',
127
+ REPLICATE: 'AI / Replicate',
128
+ HUGGINGFACE: 'AI / HuggingFace',
129
+ HF: 'AI / HuggingFace',
130
+ TOGETHER: 'AI / Together AI',
131
+ GROQ: 'AI / Groq',
132
+ MISTRAL: 'AI / Mistral',
133
+ PERPLEXITY: 'AI / Perplexity',
134
+ FIREWORKS: 'AI / Fireworks AI',
135
+ ANYSCALE: 'AI / Anyscale',
136
+ AI21: 'AI / AI21 Labs',
137
+ STABILITY: 'AI / Stability AI',
138
+ DEEPINFRA: 'AI / DeepInfra',
139
+ ELEVENLABS: 'AI / ElevenLabs',
140
+ ASSEMBLYAI: 'AI / AssemblyAI',
141
+ DEEPGRAM: 'AI / Deepgram',
142
+
143
+ // ── Vector DBs ─────────────────────────────────────────────────────────────
144
+ PINECONE: 'Vector DB / Pinecone',
145
+ WEAVIATE: 'Vector DB / Weaviate',
146
+ QDRANT: 'Vector DB / Qdrant',
147
+ CHROMA: 'Vector DB / Chroma',
148
+ MILVUS: 'Vector DB / Milvus',
149
+
150
+ // ── Payments ───────────────────────────────────────────────────────────────
151
+ STRIPE: 'Payments / Stripe',
152
+ PAYPAL: 'Payments / PayPal',
153
+ BRAINTREE: 'Payments / Braintree',
154
+ SQUARE: 'Payments / Square',
155
+ LEMON: 'Payments / Lemon Squeezy',
156
+ LEMONSQUEEZY: 'Payments / Lemon Squeezy',
157
+ PADDLE: 'Payments / Paddle',
158
+ COINBASE: 'Payments / Coinbase Commerce',
159
+ RAZORPAY: 'Payments / Razorpay',
160
+
161
+ // ── Email ──────────────────────────────────────────────────────────────────
162
+ SMTP: 'Email / SMTP',
163
+ MAIL: 'Email',
164
+ EMAIL: 'Email',
165
+ SENDGRID: 'Email / SendGrid',
166
+ RESEND: 'Email / Resend',
167
+ MAILGUN: 'Email / Mailgun',
168
+ POSTMARK: 'Email / Postmark',
169
+ MAILCHIMP: 'Email / Mailchimp',
170
+ MANDRILL: 'Email / Mandrill',
171
+ SES: 'Email / AWS SES',
172
+ SPARKPOST: 'Email / SparkPost',
173
+ CONVERTKIT: 'Email / ConvertKit',
174
+ LOOPS: 'Email / Loops',
175
+
176
+ // ── SMS / Communications ───────────────────────────────────────────────────
177
+ TWILIO: 'SMS / Twilio',
178
+ VONAGE: 'SMS / Vonage',
179
+ MESSAGEBIRD: 'SMS / MessageBird',
180
+ TELNYX: 'SMS / Telnyx',
181
+ SINCH: 'SMS / Sinch',
182
+ BANDWIDTH: 'SMS / Bandwidth',
183
+
184
+ // ── Search ─────────────────────────────────────────────────────────────────
185
+ ALGOLIA: 'Search / Algolia',
186
+ MEILISEARCH: 'Search / Meilisearch',
187
+ TYPESENSE: 'Search / Typesense',
188
+ ELASTICSEARCH: 'Search / Elasticsearch',
189
+ OPENSEARCH: 'Search / OpenSearch',
190
+
191
+ // ── Storage / CDN ──────────────────────────────────────────────────────────
192
+ CLOUDINARY: 'Storage / Cloudinary',
193
+ IMAGEKIT: 'Storage / ImageKit',
194
+ UPLOADTHING: 'Storage / UploadThing',
195
+ BUNNY: 'Storage / Bunny CDN',
196
+ BACKBLAZE: 'Storage / Backblaze B2',
197
+ R2: 'Storage / Cloudflare R2',
198
+ TIGRIS: 'Storage / Tigris',
199
+
200
+ // ── Observability ──────────────────────────────────────────────────────────
201
+ SENTRY: 'Observability / Sentry',
202
+ DATADOG: 'Observability / Datadog',
203
+ NEWRELIC: 'Observability / New Relic',
204
+ NEW_RELIC: 'Observability / New Relic',
205
+ GRAFANA: 'Observability / Grafana',
206
+ PROMETHEUS: 'Observability / Prometheus',
207
+ LOGTAIL: 'Observability / Logtail',
208
+ AXIOM: 'Observability / Axiom',
209
+ BETTERSTACK: 'Observability / Better Stack',
210
+ LOGFLARE: 'Observability / Logflare',
211
+ HIGHLIGHT: 'Observability / Highlight',
212
+ BASELIME: 'Observability / Baselime',
213
+
214
+ // ── Analytics ──────────────────────────────────────────────────────────────
215
+ POSTHOG: 'Analytics / PostHog',
216
+ SEGMENT: 'Analytics / Segment',
217
+ MIXPANEL: 'Analytics / Mixpanel',
218
+ AMPLITUDE: 'Analytics / Amplitude',
219
+ HEAP: 'Analytics / Heap',
220
+ HOTJAR: 'Analytics / Hotjar',
221
+ GA: 'Analytics / Google Analytics',
222
+ GOOGLE_ANALYTICS: 'Analytics / Google Analytics',
223
+ PLAUSIBLE: 'Analytics / Plausible',
224
+ FATHOM: 'Analytics / Fathom',
225
+ PIRSCH: 'Analytics / Pirsch',
226
+ UMAMI: 'Analytics / Umami',
227
+ JUNE: 'Analytics / June',
228
+ OPENPANEL: 'Analytics / OpenPanel',
229
+
230
+ // ── Feature Flags ──────────────────────────────────────────────────────────
231
+ LAUNCHDARKLY: 'Feature Flags / LaunchDarkly',
232
+ GROWTHBOOK: 'Feature Flags / GrowthBook',
233
+ FLAGSMITH: 'Feature Flags / Flagsmith',
234
+ STATSIG: 'Feature Flags / Statsig',
235
+ UNLEASH: 'Feature Flags / Unleash',
236
+ HYPERTUNE: 'Feature Flags / Hypertune',
237
+
238
+ // ── CMS ────────────────────────────────────────────────────────────────────
239
+ CONTENTFUL: 'CMS / Contentful',
240
+ SANITY: 'CMS / Sanity',
241
+ STRAPI: 'CMS / Strapi',
242
+ DIRECTUS: 'CMS / Directus',
243
+ PAYLOAD: 'CMS / Payload',
244
+ PRISMIC: 'CMS / Prismic',
245
+ GHOST: 'CMS / Ghost',
246
+ STORYBLOK: 'CMS / Storyblok',
247
+ BUILDER: 'CMS / Builder.io',
248
+
249
+ // ── Hosting / Deploy ───────────────────────────────────────────────────────
250
+ VERCEL: 'Hosting / Vercel',
251
+ NETLIFY: 'Hosting / Netlify',
252
+ RAILWAY: 'Hosting / Railway',
253
+ RENDER: 'Hosting / Render',
254
+ FLY: 'Hosting / Fly.io',
255
+ COOLIFY: 'Hosting / Coolify',
256
+ DOKKU: 'Hosting / Dokku',
257
+ HEROKU: 'Hosting / Heroku',
258
+ DENO: 'Hosting / Deno Deploy',
259
+ CLOUDFLARE: 'Hosting / Cloudflare',
260
+
261
+ // ── Social / OAuth Providers ───────────────────────────────────────────────
262
+ GITHUB: 'OAuth / GitHub',
263
+ GITLAB: 'OAuth / GitLab',
264
+ TWITTER: 'OAuth / Twitter / X',
265
+ TWITTER_X: 'OAuth / Twitter / X',
266
+ X: 'OAuth / Twitter / X',
267
+ FACEBOOK: 'OAuth / Facebook',
268
+ INSTAGRAM: 'OAuth / Instagram',
269
+ LINKEDIN: 'OAuth / LinkedIn',
270
+ DISCORD: 'OAuth / Discord',
271
+ SPOTIFY: 'OAuth / Spotify',
272
+ APPLE: 'OAuth / Apple',
273
+ MICROSOFT: 'OAuth / Microsoft',
274
+
275
+ // ── Collaboration / Productivity ───────────────────────────────────────────
276
+ SLACK: 'Integrations / Slack',
277
+ NOTION: 'Integrations / Notion',
278
+ LINEAR: 'Integrations / Linear',
279
+ JIRA: 'Integrations / Jira',
280
+ AIRTABLE: 'Integrations / Airtable',
281
+ ZAPIER: 'Integrations / Zapier',
282
+ MAKE: 'Integrations / Make',
283
+
284
+ // ── Maps / Location ────────────────────────────────────────────────────────
285
+ MAPBOX: 'Maps / Mapbox',
286
+ GOOGLE_MAPS: 'Maps / Google Maps',
287
+ MAPS: 'Maps / Google Maps',
288
+ HERE: 'Maps / HERE',
289
+ IPINFO: 'Maps / IPinfo',
290
+ MAXMIND: 'Maps / MaxMind',
291
+
292
+ // ── Crypto / Web3 ──────────────────────────────────────────────────────────
293
+ ALCHEMY: 'Web3 / Alchemy',
294
+ INFURA: 'Web3 / Infura',
295
+ MORALIS: 'Web3 / Moralis',
296
+ THIRDWEB: 'Web3 / Thirdweb',
297
+ WALLET: 'Web3 / Wallet',
298
+
299
+ // ── Testing / Dev Tools ────────────────────────────────────────────────────
300
+ PLAYWRIGHT: 'Testing / Playwright',
301
+ CYPRESS: 'Testing / Cypress',
302
+ BROWSERSTACK: 'Testing / BrowserStack',
303
+ SAUCE: 'Testing / Sauce Labs',
304
+ STORYBOOK: 'Dev / Storybook',
305
+ CHROMATIC: 'Dev / Chromatic',
306
+ };
307
+
308
+ const STANDALONE_LABELS = {
309
+ PORT: 'App / Config',
310
+ HOST: 'App / Config',
311
+ NODE_ENV: 'App / Config',
312
+ APP_ENV: 'App / Config',
313
+ ENVIRONMENT: 'App / Config',
314
+ BASE_URL: 'App / Config',
315
+ API_URL: 'App / Config',
316
+ SITE_URL: 'App / Config',
317
+ FRONTEND_URL: 'App / Config',
318
+ BACKEND_URL: 'App / Config',
319
+ PUBLIC_URL: 'App / Config',
320
+ LOG_LEVEL: 'App / Config',
321
+ DEBUG: 'App / Config',
322
+ TZ: 'App / Config',
323
+ TIMEZONE: 'App / Config',
324
+ LOCALE: 'App / Config',
325
+ LANG: 'App / Config',
326
+ SECRET: 'Secrets',
327
+ SECRET_KEY: 'Secrets',
328
+ ENCRYPTION_KEY: 'Secrets',
329
+ API_KEY: 'API Keys',
330
+ API_SECRET: 'API Keys',
331
+ ACCESS_TOKEN: 'API Keys',
332
+ PRIVATE_KEY: 'Secrets',
333
+ PUBLIC_KEY: 'Secrets',
334
+ };
335
+
3
336
  export function parseEnv(content) {
4
337
  const vars = {};
5
338
  for (const line of content.split('\n')) {
@@ -9,7 +342,6 @@ export function parseEnv(content) {
9
342
  if (eqIdx === -1) continue;
10
343
  const key = trimmed.slice(0, eqIdx).trim();
11
344
  let value = trimmed.slice(eqIdx + 1).trim();
12
- // Strip surrounding quotes
13
345
  if (
14
346
  (value.startsWith('"') && value.endsWith('"')) ||
15
347
  (value.startsWith("'") && value.endsWith("'"))
@@ -21,6 +353,7 @@ export function parseEnv(content) {
21
353
  return vars;
22
354
  }
23
355
 
356
+ // Used internally for encryption — keeps insertion order, no formatting
24
357
  export function stringifyEnv(vars) {
25
358
  const lines = Object.entries(vars).map(([k, v]) => {
26
359
  const needsQuotes = /[\s"'\\#]/.test(v) || v === '';
@@ -30,19 +363,95 @@ export function stringifyEnv(vars) {
30
363
  return lines.join('\n') + (lines.length ? '\n' : '');
31
364
  }
32
365
 
366
+ function formatValue(v) {
367
+ const needsQuotes = /[\s"'\\#]/.test(v) || v === '';
368
+ const escaped = v.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
369
+ return needsQuotes ? `"${escaped}"` : v;
370
+ }
371
+
372
+ function getSectionForKey(key) {
373
+ if (STANDALONE_LABELS[key]) return STANDALONE_LABELS[key];
374
+
375
+ // Try progressively shorter prefixes — longest match wins
376
+ const parts = key.split('_');
377
+ for (let len = parts.length - 1; len >= 1; len--) {
378
+ const prefix = parts.slice(0, len).join('_');
379
+ if (SECTION_LABELS[prefix]) return SECTION_LABELS[prefix];
380
+ }
381
+
382
+ return 'General';
383
+ }
384
+
385
+ function groupAndSort(vars) {
386
+ const sections = {};
387
+
388
+ for (const [key, value] of Object.entries(vars)) {
389
+ const section = getSectionForKey(key);
390
+ if (!sections[section]) sections[section] = [];
391
+ sections[section].push([key, value]);
392
+ }
393
+
394
+ for (const section of Object.keys(sections)) {
395
+ sections[section].sort(([a], [b]) => a.localeCompare(b));
396
+ }
397
+
398
+ // App/Config first, Secrets near top, General at bottom
399
+ const priority = {
400
+ 'App / Config': 0,
401
+ 'App / Runtime': 1,
402
+ 'App / Server': 2,
403
+ 'Secrets': 3,
404
+ 'API Keys': 4,
405
+ 'General': 999,
406
+ };
407
+
408
+ return Object.entries(sections).sort(([a], [b]) => {
409
+ const pa = priority[a] ?? 50;
410
+ const pb = priority[b] ?? 50;
411
+ if (pa !== pb) return pa - pb;
412
+ return a.localeCompare(b);
413
+ });
414
+ }
415
+
33
416
  export function readEnvFile(filePath) {
34
417
  if (!existsSync(filePath)) return {};
35
418
  return parseEnv(readFileSync(filePath, 'utf8'));
36
419
  }
37
420
 
38
421
  export function writeEnvFile(filePath, vars, { envName, projectRoot } = {}) {
39
- let content = '';
422
+ const entries = Object.entries(vars);
423
+ const projectName = projectRoot ? projectRoot.split('/').pop() : null;
424
+ const lines = [];
425
+
426
+ // Header
40
427
  if (envName) {
41
- const projectName = projectRoot ? projectRoot.split('/').pop() : null;
42
- content += `# Generated by envgit from [${envName}]`;
43
- if (projectName) content += ` (${projectName})`;
44
- content += '\n# Do not edit directly use envgit set to update values\n\n';
428
+ const width = 50;
429
+ const bar = '─'.repeat(width);
430
+ lines.push(`# ${bar}`);
431
+ lines.push(`# envgitencrypted environment manager`);
432
+ if (projectName) lines.push(`# Project : ${projectName}`);
433
+ lines.push(`# Env : ${envName}`);
434
+ lines.push(`# Edit : envgit set KEY=VALUE --env ${envName}`);
435
+ lines.push(`# ${bar}`);
436
+ lines.push('');
437
+ }
438
+
439
+ if (entries.length === 0) {
440
+ writeFileSync(filePath, lines.join('\n'), 'utf8');
441
+ return;
442
+ }
443
+
444
+ const grouped = groupAndSort(vars);
445
+ const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
446
+
447
+ for (const [section, sectionVars] of grouped) {
448
+ lines.push(`# ── ${section} ${'─'.repeat(Math.max(0, 44 - section.length))}`);
449
+ for (const [k, v] of sectionVars) {
450
+ const padding = ' '.repeat(maxKeyLen - k.length);
451
+ lines.push(`${k}${padding} = ${formatValue(v)}`);
452
+ }
453
+ lines.push('');
45
454
  }
46
- content += stringifyEnv(vars);
47
- writeFileSync(filePath, content, 'utf8');
455
+
456
+ writeFileSync(filePath, lines.join('\n'), 'utf8');
48
457
  }