flecto 1.0.2 → 2.0.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Flecto watches your config files and tells you exactly what changed — in plain English.**
4
4
 
5
- No more staring at raw line diffs. When your `.env`, `YAML`, `JSON`, or `TOML` file changes, Flecto shows you what actually happened:
5
+ No more staring at raw line diffs. When your `.env`, `YAML`, `JSON`, `TOML`, or `INI` file changes, Flecto shows you what actually happened:
6
6
 
7
7
  ```
8
8
  [10:42:31] config/prod.yaml — 3 changes
@@ -99,17 +99,67 @@ Each webhook payload includes a full event envelope:
99
99
 
100
100
  ```json
101
101
  {
102
- "schema_version": "1.1",
102
+ "schema_version": "2.0",
103
103
  "event_id": "uuid",
104
104
  "event_type": "changes",
105
105
  "emitted_at": "2026-04-14T10:42:31.000Z",
106
106
  "file": "/absolute/path/to/config/prod.yaml",
107
107
  "changes": [
108
108
  { "type": "changed", "path": "database.pool_size", "before": 5, "after": 20 }
109
+ ],
110
+ "policies": [
111
+ {
112
+ "id": "pool-size-jump",
113
+ "severity": "warn",
114
+ "path": "database.pool_size",
115
+ "message": "Pool size increased from 5 to 20 (>=2x).",
116
+ "pack": "default"
117
+ }
109
118
  ]
110
119
  }
111
120
  ```
112
121
 
122
+ Envelope JSON Schema: [`schemas/flecto-envelope-2.0.json`](schemas/flecto-envelope-2.0.json).
123
+
124
+ ### Policy packs and profiles
125
+
126
+ ```bash
127
+ flecto ci config/prod.yaml --profile prod --snapshot-ref HEAD~1
128
+ ```
129
+
130
+ `.flectorc.json` example:
131
+
132
+ ```json
133
+ {
134
+ "defaults": {
135
+ "policies": ["default"],
136
+ "maskSecrets": false
137
+ },
138
+ "profiles": {
139
+ "prod": {
140
+ "policies": ["default", "strict-prod"],
141
+ "maskSecrets": true
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ Profile selection: `--profile` > `FLECTO_PROFILE` > defaults. Custom packs live in `policies/<id>.json`. Local ESM plugins export `evaluate(changes, ctx)`.
148
+
149
+ ### Opt-in array identity matching
150
+
151
+ ```bash
152
+ flecto watch config/services.yaml --array-id-key id
153
+ ```
154
+
155
+ Without the flag, arrays still diff by index (1.x behavior).
156
+
157
+ ### Migrating from envelope 1.1
158
+
159
+ - `schema_version` is now `"2.0"`
160
+ - Envelope type name is `FlectoEnvelope` (was `SentinelEnvelope` in docs/types only)
161
+ - New `policies` array on change envelopes
162
+ - Webhook headers are unchanged (`X-Flecto-*`)
113
163
  ### Use both command and webhook together
114
164
 
115
165
  ```bash
package/index.js CHANGED
@@ -11,11 +11,26 @@ import chalk from 'chalk';
11
11
  import { parseFile, isSupported, parseContent } from './src/parser.js';
12
12
  import { diffTrees } from './src/differ.js';
13
13
  import { startWatcher } from './src/watcher.js';
14
- import { renderChanges, renderDiff, renderError, renderInfo, renderWarn, renderPolicyFindings } from './src/renderer.js';
14
+ import {
15
+ renderChanges,
16
+ renderDiff,
17
+ renderError,
18
+ renderInfo,
19
+ renderWarn,
20
+ renderPolicyFindings,
21
+ maskChangeEvent,
22
+ } from './src/renderer.js';
15
23
  import { fireAlerts } from './src/alerter.js';
16
24
  import { createEnvelope } from './src/envelope.js';
17
25
  import { evaluatePolicies, highestSeverity } from './src/policy.js';
18
- import { loadRcConfig, resolveEffectiveOptions, resolveFiles, initRcFile } from './src/config.js';
26
+ import {
27
+ loadRcConfig,
28
+ resolveEffectiveOptions,
29
+ resolveFiles,
30
+ initRcFile,
31
+ resolveProfileName,
32
+ resolvePolicyOptions,
33
+ } from './src/config.js';
19
34
 
20
35
  const PKG = JSON.parse(
21
36
  readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8'),
@@ -24,7 +39,6 @@ const PKG = JSON.parse(
24
39
  const SNAPSHOT_DIR = '.flecto-snapshots';
25
40
 
26
41
  function snapshotIdForPath(absPath) {
27
- // Stable across platforms and avoids basename collisions
28
42
  const normalized = absPath.replaceAll('\\', '/');
29
43
  return createHash('sha256').update(normalized).digest('hex').slice(0, 16);
30
44
  }
@@ -66,6 +80,39 @@ function validateInterval(interval) {
66
80
  }
67
81
  }
68
82
 
83
+ function stripUnsetCliOverrides(opts) {
84
+ const out = { ...opts };
85
+ // Don't let Commander defaults wipe .flectorc values for optional features
86
+ for (const key of [
87
+ 'policies',
88
+ 'plugins',
89
+ 'arrayIdKey',
90
+ 'maskSecrets',
91
+ 'maskSecretsWebhooks',
92
+ 'arrayIgnoreOrder',
93
+ 'snapshotRef',
94
+ 'ignore',
95
+ ]) {
96
+ if (out[key] === undefined || out[key] === false || out[key] === null || out[key] === '') {
97
+ delete out[key];
98
+ }
99
+ }
100
+ return out;
101
+ }
102
+
103
+ function diffOptionsFromEffective(effective, ignorePaths) {
104
+ return {
105
+ ignorePaths,
106
+ arrayIdKey: effective.arrayIdKey || null,
107
+ arrayIgnoreOrder: Boolean(effective.arrayIgnoreOrder),
108
+ };
109
+ }
110
+
111
+ function maybeMaskChanges(events, maskSecrets) {
112
+ if (!maskSecrets) return events;
113
+ return events.map(maskChangeEvent);
114
+ }
115
+
69
116
  async function resolveTargetFiles(cliFiles, rcConfig) {
70
117
  if (cliFiles && cliFiles.length > 0) {
71
118
  const direct = [];
@@ -107,7 +154,6 @@ function readSnapshotStateFromRef(filePath, snapshotRef) {
107
154
  return readSnapshotStateFromFile(maybePath);
108
155
  }
109
156
 
110
- // git ref mode: flecto ci file --snapshot-ref HEAD~1
111
157
  const rel = relative(process.cwd(), filePath).replaceAll('\\', '/');
112
158
  const raw = execFileSync('git', ['show', `${snapshotRef}:${rel}`], { encoding: 'utf8' });
113
159
  return parseContent(filePath, raw);
@@ -143,11 +189,14 @@ function printCiOutput(results, format) {
143
189
  for (const result of results) {
144
190
  for (const event of result.envelope.changes) {
145
191
  const title = `flecto ${event.type}`;
146
- console.log(`::warning file=${result.file},title=${title}::${event.path}`);
192
+ const detail = event.note ? `${event.path} (${event.note})` : event.path;
193
+ console.log(`::warning file=${result.file},title=${title}::${detail}`);
147
194
  }
148
195
  for (const finding of result.policies) {
149
196
  const level = finding.severity === 'error' ? 'error' : 'warning';
150
- console.log(`::${level} file=${result.file},title=policy::${finding.path} ${finding.message}`);
197
+ const pack = finding.pack ? ` [${finding.pack}]` : '';
198
+ const title = `flecto policy ${finding.id}${pack}`;
199
+ console.log(`::${level} file=${result.file},title=${title}::${finding.path}: ${finding.message}`);
151
200
  }
152
201
  }
153
202
  }
@@ -161,7 +210,7 @@ program
161
210
  program
162
211
  .command('watch [files...]')
163
212
  .description('Watch config files/globs for semantic changes')
164
- .option('-p, --profile <name>', 'Use profile from .flectorc')
213
+ .option('-p, --profile <name>', 'Use profile from .flectorc (else FLECTO_PROFILE)')
165
214
  .option('-i, --interval <ms>', 'Polling fallback interval in ms', '100')
166
215
  .option('--polling', 'Force polling mode (useful on network drives / some editors)', false)
167
216
  .option('-m, --mode <mode>', 'Output mode: compact | verbose', 'compact')
@@ -176,12 +225,20 @@ program
176
225
  .option('--webhook-timeout <ms>', 'Webhook timeout in ms', '5000')
177
226
  .option('--webhook-retries <n>', 'Webhook retries', '2')
178
227
  .option('--ignore <keys>', 'Comma-separated key paths to ignore (e.g. "updated_at,meta.ts")')
228
+ .option('--policies <ids>', 'Comma-separated policy pack ids (default: default)')
229
+ .option('--plugins <paths>', 'Comma-separated local ESM plugin paths')
230
+ .option('--array-id-key <key>', 'Diff arrays by this object identity key (opt-in)')
231
+ .option('--array-ignore-order', 'Treat array order as insignificant', false)
232
+ .option('--mask-secrets', 'Mask secret-like values in human output', false)
233
+ .option('--mask-secrets-webhooks', 'Also mask secrets in webhook payloads', false)
179
234
  .option('--snapshot', 'Save current state as baseline instead of watching')
180
235
  .option('--diff', 'Diff current file against saved baseline and exit')
181
236
  .action(async (files, opts) => {
182
237
  try {
183
238
  const { config } = loadRcConfig(process.cwd());
184
- const effective = resolveEffectiveOptions(config, opts.profile, opts);
239
+ const profile = resolveProfileName(opts.profile);
240
+ const effective = resolveEffectiveOptions(config, profile, stripUnsetCliOverrides(opts));
241
+ const { policies, plugins } = resolvePolicyOptions(effective);
185
242
  const targets = (await resolveTargetFiles(files, config)).map((f) => resolve(f));
186
243
  if (targets.length === 0) {
187
244
  throw new Error('No files matched. Provide files or configure .flectorc files/include.');
@@ -193,6 +250,9 @@ program
193
250
  validateInterval(interval);
194
251
  const mode = String(effective.mode ?? 'compact');
195
252
  validateMode(mode);
253
+ const maskSecrets = Boolean(effective.maskSecrets);
254
+ const maskSecretsWebhooks = Boolean(effective.maskSecretsWebhooks);
255
+ const dOpts = diffOptionsFromEffective(effective, ignorePaths);
196
256
 
197
257
  if (effective.snapshot) {
198
258
  mkdirSync(SNAPSHOT_DIR, { recursive: true });
@@ -216,8 +276,8 @@ program
216
276
  }
217
277
  const before = readSnapshotStateFromFile(snapshotPath);
218
278
  const after = parseFile(filepath);
219
- const events = diffTrees(before, after, { ignorePaths });
220
- renderDiff(filepath, events);
279
+ const events = diffTrees(before, after, dOpts);
280
+ renderDiff(filepath, events, { maskSecrets });
221
281
  if (events.length > 0) hasChanges = true;
222
282
  }
223
283
  process.exit(hasChanges ? 1 : 0);
@@ -237,17 +297,32 @@ program
237
297
  renderInfo(`flecto watching ${chalk.cyan(filepath)}`);
238
298
  const watcher = startWatcher(
239
299
  filepath,
240
- { interval, mode, ignorePaths, polling: Boolean(effective.polling) },
300
+ { interval, mode, ignorePaths, polling: Boolean(effective.polling), ...dOpts },
241
301
  async (event) => {
242
302
  if (event.kind === 'changes') {
243
- renderChanges(event.filepath, event.events, mode);
244
- const policyFindings = evaluatePolicies(event.events);
303
+ renderChanges(event.filepath, event.events, mode, { maskSecrets });
304
+ let policyFindings = [];
305
+ try {
306
+ policyFindings = await evaluatePolicies(event.events, {
307
+ cwd: process.cwd(),
308
+ file: event.filepath,
309
+ profile: profile ?? null,
310
+ source: 'watch',
311
+ policies,
312
+ plugins,
313
+ });
314
+ } catch (err) {
315
+ renderError(`policy evaluation failed: ${err.message}`);
316
+ if (String(effective.onAlertFailure) === 'exit') process.exitCode = 1;
317
+ }
245
318
  renderPolicyFindings(policyFindings);
246
319
  if (effective.command || effective.webhook) {
320
+ const outboundChanges = maybeMaskChanges(event.events, maskSecretsWebhooks);
247
321
  const envelope = createEnvelope({
248
322
  source: 'watch',
249
323
  file: event.filepath,
250
- changes: event.events,
324
+ changes: outboundChanges,
325
+ policies: policyFindings,
251
326
  });
252
327
  await fireAlerts({
253
328
  command: effective.command,
@@ -306,15 +381,22 @@ program
306
381
  program
307
382
  .command('ci [files...]')
308
383
  .description('Run semantic diff in CI mode')
309
- .option('-p, --profile <name>', 'Use profile from .flectorc')
384
+ .option('-p, --profile <name>', 'Use profile from .flectorc (else FLECTO_PROFILE)')
310
385
  .option('--snapshot-ref <ref>', 'Snapshot reference: snapshot path or git ref')
311
386
  .option('--format <type>', 'Output format: json | ndjson | github-annotations', 'json')
312
387
  .option('--fail-on <rules>', 'Comma-separated fail rules: changed,added,removed,policy,error,warn', 'changed,policy,error')
313
388
  .option('--ignore <keys>', 'Comma-separated key paths to ignore')
389
+ .option('--policies <ids>', 'Comma-separated policy pack ids')
390
+ .option('--plugins <paths>', 'Comma-separated local ESM plugin paths')
391
+ .option('--array-id-key <key>', 'Diff arrays by this object identity key (opt-in)')
392
+ .option('--array-ignore-order', 'Treat array order as insignificant', false)
393
+ .option('--mask-secrets', 'Mask secret-like values in CI output', false)
314
394
  .action(async (files, opts) => {
315
395
  try {
316
396
  const { config } = loadRcConfig(process.cwd());
317
- const effective = resolveEffectiveOptions(config, opts.profile, opts);
397
+ const profile = resolveProfileName(opts.profile);
398
+ const effective = resolveEffectiveOptions(config, profile, stripUnsetCliOverrides(opts));
399
+ const { policies: packIds, plugins } = resolvePolicyOptions(effective);
318
400
  const targets = (await resolveTargetFiles(files, config)).map((f) => resolve(f));
319
401
  if (targets.length === 0) {
320
402
  throw new Error('No files matched. Provide files or configure .flectorc files/include.');
@@ -326,6 +408,8 @@ program
326
408
  if (!['json', 'ndjson', 'github-annotations'].includes(format)) {
327
409
  throw new Error('--format must be json, ndjson, or github-annotations');
328
410
  }
411
+ const maskSecrets = Boolean(effective.maskSecrets);
412
+ const dOpts = diffOptionsFromEffective(effective, ignorePaths);
329
413
 
330
414
  /** @type {any[]} */
331
415
  const results = [];
@@ -334,22 +418,34 @@ program
334
418
  for (const filepath of targets) {
335
419
  if (!existsSync(filepath) || !isSupported(filepath)) continue;
336
420
  const after = parseFile(filepath);
337
- let before = {};
421
+ let before;
338
422
  try {
339
423
  before = readSnapshotStateFromRef(filepath, effective.snapshotRef);
340
- } catch {
341
- before = {};
424
+ } catch (err) {
425
+ throw new Error(
426
+ `Failed to resolve snapshot baseline for "${filepath}"` +
427
+ `${effective.snapshotRef ? ` (ref: ${effective.snapshotRef})` : ''}: ${err.message}`
428
+ );
342
429
  }
343
- const events = diffTrees(before, after, { ignorePaths });
344
- const policies = evaluatePolicies(events);
430
+ const events = diffTrees(before, after, dOpts);
431
+ const policyFindings = await evaluatePolicies(events, {
432
+ cwd: process.cwd(),
433
+ file: filepath,
434
+ profile: profile ?? null,
435
+ source: 'ci',
436
+ policies: packIds,
437
+ plugins,
438
+ });
439
+ const outboundChanges = maybeMaskChanges(events, maskSecrets);
345
440
  const envelope = createEnvelope({
346
441
  source: 'ci',
347
442
  file: filepath,
348
- changes: events,
443
+ changes: outboundChanges,
444
+ policies: policyFindings,
349
445
  });
350
- results.push({ file: filepath, envelope, policies });
446
+ results.push({ file: filepath, envelope, policies: policyFindings });
351
447
 
352
- if (shouldFailFromChanges(events, failOn) || shouldFailFromPolicy(policies, failOn)) {
448
+ if (shouldFailFromChanges(events, failOn) || shouldFailFromPolicy(policyFindings, failOn)) {
353
449
  shouldFail = true;
354
450
  }
355
451
  }
@@ -393,6 +489,7 @@ program
393
489
  throw new Error('Global fetch unavailable. Use Node.js >= 18.');
394
490
  }
395
491
  renderInfo('fetch: available');
492
+ renderInfo(`version: ${PKG.version}`);
396
493
  renderInfo('doctor: OK');
397
494
  } catch (err) {
398
495
  renderError(`doctor failed: ${err.message}`);
@@ -402,7 +499,6 @@ program
402
499
 
403
500
  program.parse(process.argv);
404
501
 
405
- // Show help if no command given
406
502
  if (!process.argv.slice(2).length) {
407
503
  program.help();
408
504
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public",
5
5
  "provenance": true
6
6
  },
7
- "version": "1.0.2",
7
+ "version": "2.0.0",
8
8
  "description": "Flecto — semantic config watcher that reports meaningful changes in plain English",
9
9
  "license": "MIT",
10
10
  "keywords": [
@@ -35,6 +35,7 @@
35
35
  "files": [
36
36
  "index.js",
37
37
  "src/**/*",
38
+ "schemas/**/*",
38
39
  "README.md",
39
40
  "LICENSE"
40
41
  ],
@@ -0,0 +1,65 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/myselfsiddharth/Flecto/schemas/flecto-envelope-2.0.json",
4
+ "title": "FlectoEnvelope",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "event_id",
10
+ "batch_id",
11
+ "event_type",
12
+ "source",
13
+ "emitted_at",
14
+ "file",
15
+ "changes"
16
+ ],
17
+ "properties": {
18
+ "schema_version": { "const": "2.0" },
19
+ "event_id": { "type": "string", "minLength": 1 },
20
+ "batch_id": { "type": "string", "minLength": 1 },
21
+ "event_type": { "enum": ["changes", "lifecycle"] },
22
+ "source": { "enum": ["watch", "ci", "diff"] },
23
+ "emitted_at": { "type": "string", "format": "date-time" },
24
+ "file": { "type": "string" },
25
+ "changes": {
26
+ "type": "array",
27
+ "items": {
28
+ "type": "object",
29
+ "required": ["type", "path"],
30
+ "properties": {
31
+ "type": { "enum": ["added", "removed", "changed"] },
32
+ "path": { "type": "string" },
33
+ "before": true,
34
+ "after": true,
35
+ "note": { "type": "string" }
36
+ },
37
+ "additionalProperties": false
38
+ }
39
+ },
40
+ "policies": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "object",
44
+ "required": ["id", "severity", "path", "message"],
45
+ "properties": {
46
+ "id": { "type": "string" },
47
+ "severity": { "enum": ["info", "warn", "error"] },
48
+ "path": { "type": "string" },
49
+ "message": { "type": "string" },
50
+ "pack": { "type": "string" }
51
+ },
52
+ "additionalProperties": false
53
+ }
54
+ },
55
+ "lifecycle": {
56
+ "type": "object",
57
+ "required": ["type", "message"],
58
+ "properties": {
59
+ "type": { "type": "string" },
60
+ "message": { "type": "string" }
61
+ },
62
+ "additionalProperties": false
63
+ }
64
+ }
65
+ }
package/src/alerter.js CHANGED
@@ -20,7 +20,7 @@ function enqueue(fn) {
20
20
  }
21
21
 
22
22
  /**
23
- * @param {import('./envelope.js').SentinelEnvelope} envelope
23
+ * @param {import('./envelope.js').FlectoEnvelope} envelope
24
24
  */
25
25
  function buildCommandEnv(envelope) {
26
26
  const json = JSON.stringify(envelope.changes);
@@ -54,7 +54,7 @@ function buildCommandEnv(envelope) {
54
54
 
55
55
  /**
56
56
  * @param {string} command
57
- * @param {import('./envelope.js').SentinelEnvelope} envelope
57
+ * @param {import('./envelope.js').FlectoEnvelope} envelope
58
58
  * @returns {Promise<boolean>}
59
59
  */
60
60
  export function runCommand(command, envelope) {
@@ -93,7 +93,7 @@ function sleep(ms) {
93
93
  }
94
94
 
95
95
  /**
96
- * @param {import('./envelope.js').SentinelEnvelope} envelope
96
+ * @param {import('./envelope.js').FlectoEnvelope} envelope
97
97
  */
98
98
  function enqueuePersistent(envelope) {
99
99
  mkdirSync(ALERT_QUEUE_DIR, { recursive: true });
@@ -102,7 +102,7 @@ function enqueuePersistent(envelope) {
102
102
  }
103
103
 
104
104
  /**
105
- * @param {(envelope: import('./envelope.js').SentinelEnvelope) => Promise<boolean>} deliver
105
+ * @param {(envelope: import('./envelope.js').FlectoEnvelope) => Promise<boolean>} deliver
106
106
  */
107
107
  async function flushPersistentQueue(deliver) {
108
108
  try {
@@ -133,7 +133,7 @@ async function flushPersistentQueue(deliver) {
133
133
 
134
134
  /**
135
135
  * @param {string} url
136
- * @param {import('./envelope.js').SentinelEnvelope} envelope
136
+ * @param {import('./envelope.js').FlectoEnvelope} envelope
137
137
  * @param {{ headers?: Record<string, string>, timeoutMs?: number, retries?: number }} [options]
138
138
  * @returns {Promise<boolean>}
139
139
  */
@@ -186,7 +186,7 @@ export async function postWebhook(url, envelope, options = {}) {
186
186
 
187
187
  /**
188
188
  * @param {{ webhook?: string, webhookHeaders?: Record<string, string>, webhookTimeoutMs?: number, webhookRetries?: number }} options
189
- * @param {import('./envelope.js').SentinelEnvelope} envelope
189
+ * @param {import('./envelope.js').FlectoEnvelope} envelope
190
190
  */
191
191
  async function deliverWebhook(options, envelope) {
192
192
  if (!options.webhook) return true;
@@ -219,7 +219,7 @@ function applyFailurePolicy(options, ok) {
219
219
  * deliveryMode?: 'best-effort' | 'at-least-once',
220
220
  * onAlertFailure?: 'warn' | 'exit' | 'retry'
221
221
  * }} options
222
- * @param {import('./envelope.js').SentinelEnvelope} envelope
222
+ * @param {import('./envelope.js').FlectoEnvelope} envelope
223
223
  * @returns {Promise<{ ok: boolean }>}
224
224
  */
225
225
  export async function fireAlerts(options, envelope) {
package/src/config.js CHANGED
@@ -43,6 +43,17 @@ export function loadRcConfig(cwd = process.cwd()) {
43
43
  return { path: null, config: null };
44
44
  }
45
45
 
46
+ /**
47
+ * Resolve profile name: CLI > FLECTO_PROFILE > none.
48
+ * @param {string | undefined} cliProfile
49
+ * @returns {string | undefined}
50
+ */
51
+ export function resolveProfileName(cliProfile) {
52
+ if (cliProfile) return String(cliProfile);
53
+ if (process.env.FLECTO_PROFILE) return String(process.env.FLECTO_PROFILE);
54
+ return undefined;
55
+ }
56
+
46
57
  /**
47
58
  * Resolve effective options with optional profile and CLI overrides.
48
59
  * @param {FlectoRc | null} config
@@ -55,6 +66,26 @@ export function resolveEffectiveOptions(config, profile, cliOverrides = {}) {
55
66
  return { ...defaults, ...profileOptions, ...cliOverrides };
56
67
  }
57
68
 
69
+ /**
70
+ * Normalize policy-related effective options.
71
+ * @param {Record<string, unknown>} effective
72
+ */
73
+ export function resolvePolicyOptions(effective) {
74
+ const policiesRaw = effective.policies;
75
+ const pluginsRaw = effective.plugins;
76
+ const policies = Array.isArray(policiesRaw)
77
+ ? policiesRaw.map(String)
78
+ : typeof policiesRaw === 'string'
79
+ ? String(policiesRaw).split(',').map((s) => s.trim()).filter(Boolean)
80
+ : ['default'];
81
+ const plugins = Array.isArray(pluginsRaw)
82
+ ? pluginsRaw.map(String)
83
+ : typeof pluginsRaw === 'string'
84
+ ? String(pluginsRaw).split(',').map((s) => s.trim()).filter(Boolean)
85
+ : [];
86
+ return { policies, plugins };
87
+ }
88
+
58
89
  /**
59
90
  * Expand file patterns from rc include/files and direct CLI inputs.
60
91
  * @param {{ cwd?: string, files?: string[], include?: string[], exclude?: string[] }} input
@@ -93,15 +124,20 @@ export function initRcFile(cwd = process.cwd()) {
93
124
  ignore: ['**.updated_at'],
94
125
  deliveryMode: 'best-effort',
95
126
  onAlertFailure: 'warn',
127
+ policies: ['default'],
128
+ plugins: [],
129
+ arrayIdKey: null,
130
+ arrayIgnoreOrder: false,
131
+ maskSecrets: false,
96
132
  },
97
133
  profiles: {
98
134
  dev: { mode: 'verbose' },
99
135
  ci: { failOn: 'policy,error' },
136
+ prod: { policies: ['default', 'strict-prod'], maskSecrets: true },
100
137
  },
101
- files: ['config/**/*.yaml', '.env'],
138
+ files: ['config/**/*.{yaml,yml,json,toml,ini}', '.env', '.env.*', '*.env'],
102
139
  exclude: ['**/node_modules/**'],
103
140
  };
104
141
  writeFileSync(path, JSON.stringify(starter, null, 2), 'utf8');
105
142
  return path;
106
143
  }
107
-