@produck/agent-toolkit 0.5.0 → 0.6.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.
@@ -20,6 +20,10 @@ import {
20
20
  printSyncPrettierConfigHelp,
21
21
  runSyncPrettierConfig,
22
22
  } from './command/sync-prettier-config/index.mjs';
23
+ import {
24
+ printSyncEditorconfigHelp,
25
+ runSyncEditorconfig,
26
+ } from './command/sync-editorconfig/index.mjs';
23
27
  import {
24
28
  printSyncEslintConfigHelp,
25
29
  runSyncEslintConfig,
@@ -79,6 +83,10 @@ const COMMANDS = {
79
83
  printHelp: printSyncInstructionsHelp,
80
84
  run: runSyncInstructions,
81
85
  },
86
+ 'sync-editorconfig': {
87
+ printHelp: printSyncEditorconfigHelp,
88
+ run: runSyncEditorconfig,
89
+ },
82
90
  };
83
91
 
84
92
  const DEFAULT_COMMAND = 'enforce-node-baseline';
@@ -127,6 +127,13 @@ export function runEnforceNodeBaseline(options) {
127
127
  syncWorkspaceConfigArgs.push('--dry-run');
128
128
  }
129
129
 
130
+ const syncEditorconfigArgs = ['sync-editorconfig', '--cwd', cwd];
131
+ if (check) {
132
+ syncEditorconfigArgs.push('--check');
133
+ } else if (dryRun) {
134
+ syncEditorconfigArgs.push('--dry-run');
135
+ }
136
+
130
137
  const syncPrettierConfigArgs = ['sync-prettier-config', '--cwd', cwd];
131
138
  if (check) {
132
139
  syncPrettierConfigArgs.push('--check');
@@ -144,6 +151,7 @@ export function runEnforceNodeBaseline(options) {
144
151
  const plan = [
145
152
  { name: 'sync-instructions', args: syncInstructionsArgs },
146
153
  { name: 'preflight', args: preflightArgs },
154
+ { name: 'sync-editorconfig', args: syncEditorconfigArgs },
147
155
  { name: 'sync-prettier-config', args: syncPrettierConfigArgs },
148
156
  { name: 'sync-eslint-config', args: syncEslintConfigArgs },
149
157
  { name: 'sync-workspace-config', args: syncWorkspaceConfigArgs },
@@ -4,6 +4,7 @@ agent-toolkit commands:
4
4
  run-capture
5
5
  summarize-log
6
6
  sync-coverage-script
7
+ sync-editorconfig
7
8
  sync-prettier-config
8
9
  sync-eslint-config
9
10
  sync-workspace-config
@@ -0,0 +1,15 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ indent_style = space
6
+ indent_size = 2
7
+ trim_trailing_whitespace = true
8
+
9
+ [*.{yml,yaml}]
10
+ indent_style = space
11
+ indent_size = 2
12
+
13
+ [*.md]
14
+ trim_trailing_whitespace = false
15
+ max_line_length = 80
@@ -0,0 +1,13 @@
1
+ Usage:
2
+ agent-toolkit sync-editorconfig [--cwd <dir>] [--check] [--dry-run]
3
+ [--json <file>]
4
+
5
+ Behavior:
6
+ - Applies organization-required root .editorconfig file
7
+ - Creates .editorconfig if missing, or merges missing required entries
8
+ if file already exists
9
+
10
+ Rules:
11
+ - --check validates without writing and exits non-zero on mismatch
12
+ - --dry-run prints planned changes without writing
13
+ - --check takes precedence over --dry-run
@@ -0,0 +1,233 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { getSingle, hasFlag } from '../shared/args.mjs';
6
+ import { printTextResource } from '../shared/text-resource.mjs';
7
+
8
+ const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
9
+ const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
10
+ const EDITORCONFIG_FILE = '.editorconfig';
11
+ const TEMPLATE_FILE = path.resolve(COMMAND_DIR, 'editorconfig.template');
12
+
13
+ const REQUIRED_EDITORCONFIG_CONTENT = fs.readFileSync(TEMPLATE_FILE, 'utf8');
14
+
15
+ // Required key-value pairs for validation
16
+ const REQUIRED_SECTIONS = {
17
+ root: {
18
+ line: 'root = true',
19
+ },
20
+ '*': {
21
+ keys: {
22
+ charset: 'utf-8',
23
+ indent_style: 'space',
24
+ indent_size: '2',
25
+ trim_trailing_whitespace: 'true',
26
+ },
27
+ },
28
+ '*.{yml,yaml}': {
29
+ keys: {
30
+ indent_style: 'space',
31
+ indent_size: '2',
32
+ },
33
+ },
34
+ '*.md': {
35
+ keys: {
36
+ trim_trailing_whitespace: 'false',
37
+ max_line_length: '80',
38
+ },
39
+ },
40
+ };
41
+
42
+ export function printSyncEditorconfigHelp() {
43
+ printTextResource(HELP_FILE);
44
+ }
45
+
46
+ function parseEditorconfig(content) {
47
+ const sections = {};
48
+ let currentSection = null;
49
+
50
+ for (const line of content.split('\n')) {
51
+ const trimmed = line.trim();
52
+
53
+ // Skip empty lines and comments
54
+ if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith(';')) {
55
+ continue;
56
+ }
57
+
58
+ // Check for section header
59
+ const sectionMatch = trimmed.match(/^\[(.+)\]$/);
60
+ if (sectionMatch) {
61
+ currentSection = sectionMatch[1];
62
+ sections[currentSection] = {};
63
+ continue;
64
+ }
65
+
66
+ // Check for root = true
67
+ const rootMatch = trimmed.match(/^root\s*=\s*(.+)$/i);
68
+ if (rootMatch && !currentSection) {
69
+ sections._root = rootMatch[1].trim().toLowerCase();
70
+ continue;
71
+ }
72
+
73
+ // Parse key-value pair
74
+ if (currentSection) {
75
+ const kvMatch = trimmed.match(/^([^=]+)\s*=\s*(.+)$/);
76
+ if (kvMatch) {
77
+ sections[currentSection][kvMatch[1].trim().toLowerCase()] = kvMatch[2].trim().toLowerCase();
78
+ }
79
+ }
80
+ }
81
+
82
+ return sections;
83
+ }
84
+
85
+ function validateEditorconfig(sections) {
86
+ const mismatches = [];
87
+
88
+ // Check root
89
+ if (sections._root !== 'true') {
90
+ mismatches.push({ section: '_root', expected: 'true', actual: sections._root || 'missing' });
91
+ }
92
+
93
+ // Check each required section
94
+ for (const [sectionName, config] of Object.entries(REQUIRED_SECTIONS)) {
95
+ if (sectionName === 'root') continue;
96
+
97
+ if (!sections[sectionName]) {
98
+ mismatches.push({ section: `[${sectionName}]`, expected: 'present', actual: 'missing' });
99
+ continue;
100
+ }
101
+
102
+ if (config.keys) {
103
+ for (const [key, expectedValue] of Object.entries(config.keys)) {
104
+ const actualValue = sections[sectionName][key];
105
+ if (actualValue !== expectedValue) {
106
+ mismatches.push({
107
+ section: `[${sectionName}]`,
108
+ key,
109
+ expected: expectedValue,
110
+ actual: actualValue || 'missing',
111
+ });
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ return mismatches;
118
+ }
119
+
120
+ function buildUpdatedContent(existingContent) {
121
+ const existingSections = parseEditorconfig(existingContent);
122
+ const lines = [];
123
+
124
+ // Add root if missing
125
+ if (existingSections._root !== 'true') {
126
+ lines.push('root = true');
127
+ }
128
+
129
+ // Process each required section
130
+ for (const [sectionName, config] of Object.entries(REQUIRED_SECTIONS)) {
131
+ if (sectionName === 'root') continue;
132
+
133
+ const existingSection = existingSections[sectionName] || {};
134
+ const missingKeys = [];
135
+
136
+ if (config.keys) {
137
+ for (const [key, expectedValue] of Object.entries(config.keys)) {
138
+ if (existingSection[key] !== expectedValue) {
139
+ missingKeys.push({ key, value: expectedValue });
140
+ }
141
+ }
142
+ }
143
+
144
+ if (missingKeys.length > 0 || !existingSections[sectionName]) {
145
+ lines.push('');
146
+ lines.push(`[${sectionName}]`);
147
+ for (const { key, value } of missingKeys) {
148
+ lines.push(`${key} = ${value}`);
149
+ }
150
+ }
151
+ }
152
+
153
+ // If no updates needed, return original
154
+ // c8 ignore next 3
155
+ if (lines.length === 0) {
156
+ return existingContent;
157
+ }
158
+
159
+ // Append missing entries to existing content
160
+ return existingContent.trimEnd() + lines.join('\n') + '\n';
161
+ }
162
+
163
+ function readFileIfExists(filePath) {
164
+ if (!fs.existsSync(filePath)) {
165
+ return null;
166
+ }
167
+
168
+ return fs.readFileSync(filePath, 'utf8');
169
+ }
170
+
171
+ export function runSyncEditorconfig(options) {
172
+ const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
173
+ const check = hasFlag(options, '--check');
174
+ const dryRun = hasFlag(options, '--dry-run') && !check;
175
+ const jsonFile = getSingle(options, '--json', '');
176
+ const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
177
+
178
+ if (!fs.existsSync(cwd)) {
179
+ console.error(`CWD does not exist: ${cwd}`);
180
+ process.exit(2);
181
+ }
182
+
183
+ const editorconfigPath = path.resolve(cwd, EDITORCONFIG_FILE);
184
+ const currentContent = readFileIfExists(editorconfigPath);
185
+ const fileExists = currentContent !== null;
186
+
187
+ const sections = currentContent ? parseEditorconfig(currentContent) : {};
188
+ const mismatches = validateEditorconfig(sections);
189
+ const requiresUpdate = mismatches.length > 0 || !fileExists;
190
+
191
+ let plannedContent = null;
192
+ if (requiresUpdate) {
193
+ plannedContent = fileExists
194
+ ? buildUpdatedContent(currentContent)
195
+ : REQUIRED_EDITORCONFIG_CONTENT;
196
+ }
197
+
198
+ if (mode === 'sync' && requiresUpdate && plannedContent) {
199
+ fs.writeFileSync(editorconfigPath, plannedContent, 'utf8');
200
+ }
201
+
202
+ const report = {
203
+ cwd,
204
+ mode,
205
+ ok: true,
206
+ editorconfigPath,
207
+ required: {
208
+ file: EDITORCONFIG_FILE,
209
+ },
210
+ status: {
211
+ fileExistsBefore: fileExists,
212
+ mismatchesBefore: mismatches,
213
+ fileExistsAfter: requiresUpdate && mode === 'sync' ? true : fileExists,
214
+ mismatchesAfter: requiresUpdate && mode === 'sync' ? [] : mismatches,
215
+ updated: requiresUpdate && mode === 'sync',
216
+ },
217
+ };
218
+
219
+ if (mode === 'check' && requiresUpdate) {
220
+ report.ok = false;
221
+ }
222
+
223
+ if (jsonFile) {
224
+ const outPath = path.resolve(cwd, jsonFile);
225
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
226
+ fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
227
+ }
228
+
229
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
230
+ if (!report.ok) {
231
+ process.exit(2);
232
+ }
233
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produck/agent-toolkit",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Central CLI toolkit for organization AI execution workflows",
5
5
  "type": "module",
6
6
  "repository": {
@@ -32,5 +32,5 @@
32
32
  "devDependencies": {
33
33
  "c8": "11.0.0"
34
34
  },
35
- "gitHead": "fb8c64919617ff5a76b1c736d1fe3903206ed131"
35
+ "gitHead": "8618167ffe15e39c5a7b350903f0fd8861902a62"
36
36
  }