@produck/agent-toolkit 0.8.1 → 0.9.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.
@@ -7,23 +7,21 @@ import { printTextResource } from '../shared/text-resource.mjs';
7
7
 
8
8
  const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
9
9
  const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
10
+ const PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
11
+ const REPO_ROOT = path.resolve(PACKAGE_ROOT, '../..');
10
12
  const LERNA_CONFIG_FILE = 'lerna.json';
13
+ const LERNA_TEMPLATE_CANDIDATE_PATHS = [
14
+ path.resolve(REPO_ROOT, LERNA_CONFIG_FILE),
15
+ path.resolve(PACKAGE_ROOT, 'publish-assets', LERNA_CONFIG_FILE),
16
+ ];
11
17
 
12
18
  const REQUIRED_PUBLISH_CHECK_SCRIPT_KEY = 'produck:publish:check';
13
19
  const REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE =
14
- 'npm run produck:install && npm run produck:format && npm run produck:lint && npm run produck:coverage';
15
- const USER_PUBLISH_SCRIPT_KEY = 'publish';
20
+ 'npm run produck:install && npm run produck:coverage && npm run produck:commit:check';
16
21
  const REQUIRED_PUBLISH_SCRIPT_KEY = 'produck:publish';
17
- const REQUIRED_PUBLISH_SCRIPT_FALLBACK_VALUE = 'npm run produck:publish:check && lerna publish';
18
-
19
- const REQUIRED_LERNA_DEFAULT_CONFIG = `${JSON.stringify(
20
- {
21
- $schema: 'node_modules/lerna/schemas/lerna-schema.json',
22
- version: 'independent',
23
- },
24
- null,
25
- 2,
26
- )}\n`;
22
+ const REQUIRED_PUBLISH_SCRIPT_VALUE =
23
+ 'npm run produck:publish:check && npm run publish --';
24
+ const REQUIRED_LERNA_VERSION_COMMIT_HOOKS = false;
27
25
 
28
26
  export function printSyncPublishHelp() {
29
27
  printTextResource(HELP_FILE);
@@ -38,12 +36,51 @@ function parseJsonFile(filePath, label) {
38
36
  }
39
37
  }
40
38
 
41
- function buildRequiredPublishScriptValue(hasUserPublishScript) {
42
- if (hasUserPublishScript) {
43
- return 'npm run produck:publish:check && npm run publish --';
39
+ function loadRequiredLernaTemplate() {
40
+ const templatePath = LERNA_TEMPLATE_CANDIDATE_PATHS.find((candidatePath) => {
41
+ return fs.existsSync(candidatePath);
42
+ });
43
+
44
+ if (!templatePath) {
45
+ console.error('lerna template does not exist in expected locations:');
46
+ for (const candidatePath of LERNA_TEMPLATE_CANDIDATE_PATHS) {
47
+ console.error(`- ${candidatePath}`);
48
+ }
49
+ process.exit(2);
50
+ }
51
+
52
+ const template = parseJsonFile(templatePath, 'lerna template');
53
+ if (typeof template.version !== 'string') {
54
+ console.error(
55
+ `lerna template must have a "version" field: ${templatePath}`,
56
+ );
57
+ process.exit(2);
44
58
  }
45
59
 
46
- return REQUIRED_PUBLISH_SCRIPT_FALLBACK_VALUE;
60
+ // The {} fallbacks below guard against templates that omit 'command' or
61
+ // 'command.version'; the canonical lerna template always provides both.
62
+ const normalizedTemplate = {
63
+ ...template,
64
+ command: {
65
+ ...(template.command && typeof template.command === 'object'
66
+ ? template.command
67
+ : /* c8 ignore next */
68
+ {}),
69
+ version: {
70
+ ...(template?.command?.version &&
71
+ typeof template.command.version === 'object'
72
+ ? template.command.version
73
+ : /* c8 ignore next */
74
+ {}),
75
+ commitHooks: REQUIRED_LERNA_VERSION_COMMIT_HOOKS,
76
+ },
77
+ },
78
+ };
79
+
80
+ return {
81
+ templatePath,
82
+ content: `${JSON.stringify(normalizedTemplate, null, 2)}\n`,
83
+ };
47
84
  }
48
85
 
49
86
  export function runSyncPublish(options) {
@@ -52,6 +89,7 @@ export function runSyncPublish(options) {
52
89
  const dryRun = hasFlag(options, '--dry-run') && !check;
53
90
  const jsonFile = getSingle(options, '--json', '');
54
91
  const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
92
+ const requiredLernaTemplate = loadRequiredLernaTemplate();
55
93
 
56
94
  if (!fs.existsSync(cwd)) {
57
95
  console.error(`CWD does not exist: ${cwd}`);
@@ -61,19 +99,53 @@ export function runSyncPublish(options) {
61
99
  const lernaConfigPath = path.resolve(cwd, LERNA_CONFIG_FILE);
62
100
  const lernaExistedBefore = fs.existsSync(lernaConfigPath);
63
101
  let lernaDefaultCreated = false;
102
+ let matchesRequiredLernaCommitHooks = false;
103
+ let matchesRequiredLernaCommitHooksBefore = false;
64
104
 
65
105
  if (!lernaExistedBefore) {
66
106
  if (mode === 'sync') {
67
- fs.writeFileSync(lernaConfigPath, REQUIRED_LERNA_DEFAULT_CONFIG, 'utf8');
107
+ fs.writeFileSync(lernaConfigPath, requiredLernaTemplate.content, 'utf8');
68
108
  lernaDefaultCreated = true;
109
+ matchesRequiredLernaCommitHooks = true;
69
110
  }
70
111
  } else {
71
112
  const lernaConfig = parseJsonFile(lernaConfigPath, 'lerna.json');
72
113
 
73
114
  if (typeof lernaConfig.version !== 'string') {
74
- console.error(`lerna.json must have a "version" field: ${lernaConfigPath}`);
115
+ console.error(
116
+ `lerna.json must have a "version" field: ${lernaConfigPath}`,
117
+ );
75
118
  process.exit(2);
76
119
  }
120
+
121
+ const currentCommitHooks = lernaConfig?.command?.version?.commitHooks;
122
+ matchesRequiredLernaCommitHooks =
123
+ currentCommitHooks === REQUIRED_LERNA_VERSION_COMMIT_HOOKS;
124
+ matchesRequiredLernaCommitHooksBefore = matchesRequiredLernaCommitHooks;
125
+
126
+ if (mode === 'sync' && !matchesRequiredLernaCommitHooks) {
127
+ const nextLernaConfig = {
128
+ ...lernaConfig,
129
+ command: {
130
+ ...(lernaConfig.command && typeof lernaConfig.command === 'object'
131
+ ? lernaConfig.command
132
+ : {}),
133
+ version: {
134
+ ...(lernaConfig?.command?.version &&
135
+ typeof lernaConfig.command.version === 'object'
136
+ ? lernaConfig.command.version
137
+ : {}),
138
+ commitHooks: REQUIRED_LERNA_VERSION_COMMIT_HOOKS,
139
+ },
140
+ },
141
+ };
142
+ fs.writeFileSync(
143
+ lernaConfigPath,
144
+ `${JSON.stringify(nextLernaConfig, null, 2)}\n`,
145
+ 'utf8',
146
+ );
147
+ matchesRequiredLernaCommitHooks = true;
148
+ }
77
149
  }
78
150
 
79
151
  const rootPackageJsonPath = path.resolve(cwd, 'package.json');
@@ -84,13 +156,11 @@ export function runSyncPublish(options) {
84
156
 
85
157
  const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
86
158
  const scripts =
87
- pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
159
+ pkg.scripts &&
160
+ typeof pkg.scripts === 'object' &&
161
+ !Array.isArray(pkg.scripts)
88
162
  ? { ...pkg.scripts }
89
163
  : {};
90
- const hasUserPublishScript =
91
- typeof scripts[USER_PUBLISH_SCRIPT_KEY] === 'string' &&
92
- scripts[USER_PUBLISH_SCRIPT_KEY].trim() !== '';
93
- const requiredPublishScriptValue = buildRequiredPublishScriptValue(hasUserPublishScript);
94
164
 
95
165
  const previousPublishCheck =
96
166
  typeof scripts[REQUIRED_PUBLISH_CHECK_SCRIPT_KEY] === 'string'
@@ -101,17 +171,30 @@ export function runSyncPublish(options) {
101
171
  ? scripts[REQUIRED_PUBLISH_SCRIPT_KEY]
102
172
  : null;
103
173
 
104
- const matchesRequiredPublishCheck = previousPublishCheck === REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE;
105
- const matchesRequiredPublish = previousPublish === requiredPublishScriptValue;
174
+ const matchesRequiredPublishCheck =
175
+ previousPublishCheck === REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE;
176
+ const matchesRequiredPublish =
177
+ previousPublish === REQUIRED_PUBLISH_SCRIPT_VALUE;
106
178
  const lernaRequiresCreation = !lernaExistedBefore && !lernaDefaultCreated;
107
179
  const requiresUpdate =
108
- !matchesRequiredPublishCheck || !matchesRequiredPublish || lernaRequiresCreation;
109
-
110
- if (mode === 'sync' && (!matchesRequiredPublishCheck || !matchesRequiredPublish)) {
111
- scripts[REQUIRED_PUBLISH_CHECK_SCRIPT_KEY] = REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE;
112
- scripts[REQUIRED_PUBLISH_SCRIPT_KEY] = requiredPublishScriptValue;
180
+ !matchesRequiredPublishCheck ||
181
+ !matchesRequiredPublish ||
182
+ lernaRequiresCreation ||
183
+ !matchesRequiredLernaCommitHooks;
184
+
185
+ if (
186
+ mode === 'sync' &&
187
+ (!matchesRequiredPublishCheck || !matchesRequiredPublish)
188
+ ) {
189
+ scripts[REQUIRED_PUBLISH_CHECK_SCRIPT_KEY] =
190
+ REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE;
191
+ scripts[REQUIRED_PUBLISH_SCRIPT_KEY] = REQUIRED_PUBLISH_SCRIPT_VALUE;
113
192
  pkg.scripts = scripts;
114
- fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
193
+ fs.writeFileSync(
194
+ rootPackageJsonPath,
195
+ `${JSON.stringify(pkg, null, 2)}\n`,
196
+ 'utf8',
197
+ );
115
198
  }
116
199
 
117
200
  const report = {
@@ -121,21 +204,29 @@ export function runSyncPublish(options) {
121
204
  lernaConfigPath,
122
205
  rootPackageJsonPath,
123
206
  required: {
207
+ lernaTemplatePath: requiredLernaTemplate.templatePath,
124
208
  publishCheckScriptKey: REQUIRED_PUBLISH_CHECK_SCRIPT_KEY,
125
209
  publishCheckScriptValue: REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE,
126
210
  publishScriptKey: REQUIRED_PUBLISH_SCRIPT_KEY,
127
- publishScriptValue: requiredPublishScriptValue,
211
+ publishScriptValue: REQUIRED_PUBLISH_SCRIPT_VALUE,
212
+ lernaVersionCommitHooks: REQUIRED_LERNA_VERSION_COMMIT_HOOKS,
128
213
  },
129
214
  status: {
130
- hasUserPublishScript,
131
215
  lernaExistedBefore,
132
216
  lernaDefaultCreated,
217
+ matchesRequiredLernaCommitHooksBefore:
218
+ matchesRequiredLernaCommitHooksBefore,
219
+ matchesRequiredLernaCommitHooksAfter: matchesRequiredLernaCommitHooks,
133
220
  matchesRequiredPublishCheckBefore: matchesRequiredPublishCheck,
134
221
  matchesRequiredPublishCheckAfter:
135
- !matchesRequiredPublishCheck && mode === 'sync' ? true : matchesRequiredPublishCheck,
222
+ !matchesRequiredPublishCheck && mode === 'sync'
223
+ ? true
224
+ : matchesRequiredPublishCheck,
136
225
  matchesRequiredPublishBefore: matchesRequiredPublish,
137
226
  matchesRequiredPublishAfter:
138
- !matchesRequiredPublish && mode === 'sync' ? true : matchesRequiredPublish,
227
+ !matchesRequiredPublish && mode === 'sync'
228
+ ? true
229
+ : matchesRequiredPublish,
139
230
  updated: requiresUpdate && mode === 'sync',
140
231
  },
141
232
  };
@@ -5,12 +5,31 @@ import { fileURLToPath } from 'node:url';
5
5
  import { getSingle } from '../shared/args.mjs';
6
6
  import { printTextResource } from '../shared/text-resource.mjs';
7
7
 
8
- const ALLOWED_TAGS = ['INIT', 'ADD', 'REMOVE', 'FIX', 'REFACTOR', 'UPGRADE', 'PUBLISH'];
9
- const ALLOWED_TARGETS = ['docs', 'test', 'ci', 'deps', 'api', 'schema', 'infra', 'fmt'];
8
+ const ALLOWED_TAGS = [
9
+ 'INIT',
10
+ 'ADD',
11
+ 'REMOVE',
12
+ 'FIX',
13
+ 'REFACTOR',
14
+ 'UPGRADE',
15
+ 'PUBLISH',
16
+ ];
17
+ const ALLOWED_TARGETS = [
18
+ 'docs',
19
+ 'test',
20
+ 'ci',
21
+ 'deps',
22
+ 'api',
23
+ 'schema',
24
+ 'infra',
25
+ 'fmt',
26
+ ];
10
27
  const SECTION_HEADER_RE = /^(?:\*|(?:@[\w.-]+\/)?[\w.-]+):$/;
11
28
  const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
12
29
  const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
13
- const ROOT_PACKAGE_FILE = path.resolve(COMMAND_DIR, '../../../../../package.json');
30
+ const ROOT_PACKAGE_FILE =
31
+ process.env._AGENT_TOOLKIT_TEST_ROOT_PKG ||
32
+ path.resolve(COMMAND_DIR, '../../../../../package.json');
14
33
  const WORKSPACE_SCOPE = 'workspace';
15
34
  const WILDCARD_SCOPE = '*';
16
35
  const DEFAULT_COMMENT_CHAR = '#';
@@ -65,42 +84,38 @@ function isMonorepoRoot() {
65
84
 
66
85
  try {
67
86
  const rootPackage = JSON.parse(fs.readFileSync(ROOT_PACKAGE_FILE, 'utf8'));
68
- return Array.isArray(rootPackage.workspaces) && rootPackage.workspaces.length > 0;
87
+ return (
88
+ Array.isArray(rootPackage.workspaces) && rootPackage.workspaces.length > 0
89
+ );
69
90
  } catch {
70
91
  return false;
71
92
  }
72
93
  }
73
94
 
74
95
  function getMonorepoAllowedSectionScopes() {
75
- if (!fs.existsSync(ROOT_PACKAGE_FILE)) {
76
- return null;
77
- }
78
-
79
- let rootPackage;
80
- try {
81
- rootPackage = JSON.parse(fs.readFileSync(ROOT_PACKAGE_FILE, 'utf8'));
82
- } catch {
83
- return null;
84
- }
85
-
86
- if (!Array.isArray(rootPackage.workspaces) || rootPackage.workspaces.length === 0) {
87
- return null;
88
- }
89
-
96
+ const rootPackage = JSON.parse(fs.readFileSync(ROOT_PACKAGE_FILE, 'utf8'));
90
97
  const rootDir = path.dirname(ROOT_PACKAGE_FILE);
91
98
  const allowedScopes = new Set([WORKSPACE_SCOPE, WILDCARD_SCOPE]);
92
99
 
93
100
  for (const workspaceEntry of rootPackage.workspaces) {
94
101
  const workspacePath = path.resolve(rootDir, String(workspaceEntry));
95
- const workspacePackageJsonPath = path.resolve(workspacePath, 'package.json');
102
+ const workspacePackageJsonPath = path.resolve(
103
+ workspacePath,
104
+ 'package.json',
105
+ );
96
106
 
97
107
  if (!fs.existsSync(workspacePackageJsonPath)) {
98
108
  continue;
99
109
  }
100
110
 
101
111
  try {
102
- const workspacePackage = JSON.parse(fs.readFileSync(workspacePackageJsonPath, 'utf8'));
103
- if (typeof workspacePackage.name === 'string' && workspacePackage.name.trim() !== '') {
112
+ const workspacePackage = JSON.parse(
113
+ fs.readFileSync(workspacePackageJsonPath, 'utf8'),
114
+ );
115
+ if (
116
+ typeof workspacePackage.name === 'string' &&
117
+ workspacePackage.name.trim() !== ''
118
+ ) {
104
119
  allowedScopes.add(workspacePackage.name.trim());
105
120
  }
106
121
  } catch {
@@ -220,7 +235,9 @@ export function runValidateCommitMsg(options) {
220
235
  }
221
236
 
222
237
  const mustUseSectionHeaders = isMonorepoRoot();
223
- const allowedSectionScopes = mustUseSectionHeaders ? getMonorepoAllowedSectionScopes() : null;
238
+ const allowedSectionScopes = mustUseSectionHeaders
239
+ ? getMonorepoAllowedSectionScopes()
240
+ : null;
224
241
  const hasSectionHeaders = lines.some((line) => isSectionHeaderLine(line));
225
242
 
226
243
  // [PUBLISH] is generated by lerna and is always a repo-wide tag.
@@ -235,11 +252,15 @@ export function runValidateCommitMsg(options) {
235
252
 
236
253
  if (mustUseSectionHeaders && !hasSectionHeaders) {
237
254
  console.error('Commit message validation failed:');
238
- console.error('- Line 1: section header is required before tagged lines in monorepo mode');
255
+ console.error(
256
+ '- Line 1: section header is required before tagged lines in monorepo mode',
257
+ );
239
258
  process.exit(1);
240
259
  }
241
260
 
242
- const errors = hasSectionHeaders ? validateSectionFormat(lines, allowedSectionScopes) : [];
261
+ const errors = hasSectionHeaders
262
+ ? validateSectionFormat(lines, allowedSectionScopes)
263
+ : [];
243
264
  if (!hasSectionHeaders) {
244
265
  for (let i = 0; i < lines.length; i += 1) {
245
266
  const err = validateCommitLine(lines[i], i + 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produck/agent-toolkit",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Central CLI toolkit for organization AI execution workflows",
5
5
  "type": "module",
6
6
  "repository": {
@@ -14,7 +14,8 @@
14
14
  "scripts": {
15
15
  "prepack": "node ./bin/build-publish-assets.mjs",
16
16
  "test": "node test/index.mjs",
17
- "produck:coverage": "c8 --reporter=lcov --reporter=html --reporter=text-summary npm test"
17
+ "produck:coverage": "c8 --reporter=lcov --reporter=html --reporter=text-summary npm test",
18
+ "produck:lint": "eslint --fix . --max-warnings=0"
18
19
  },
19
20
  "files": [
20
21
  "bin",
@@ -28,7 +29,8 @@
28
29
  },
29
30
  "license": "MIT",
30
31
  "devDependencies": {
32
+ "@produck/eslint-rules": "^0.3.6",
31
33
  "c8": "11.0.0"
32
34
  },
33
- "gitHead": "86c4556d03847ded3d7876777e778cc16a800775"
35
+ "gitHead": "d13162976bfbf23756e678027aaffe464b7c8177"
34
36
  }
@@ -0,0 +1,32 @@
1
+ import js from '@eslint/js';
2
+ import globals from 'globals';
3
+ import tseslint from 'typescript-eslint';
4
+ import json from '@eslint/json';
5
+ import markdown from '@eslint/markdown';
6
+ import { defineConfig } from 'eslint/config';
7
+ import * as ProduckRule from '@produck/eslint-rules';
8
+
9
+ export default defineConfig([
10
+ {
11
+ files: ['**/*.{js,mjs,cjs,ts,mts,cts}'],
12
+ plugins: { js },
13
+ extends: ['js/recommended'],
14
+ languageOptions: { globals: { ...globals.browser, ...globals.node } },
15
+ },
16
+ tseslint.configs.recommended,
17
+ {
18
+ files: ['**/*.json'],
19
+ ignores: ['**/package-lock.json'],
20
+ plugins: { json },
21
+ language: 'json/json',
22
+ extends: ['json/recommended'],
23
+ },
24
+ {
25
+ files: ['**/*.md'],
26
+ plugins: { markdown },
27
+ language: 'markdown/gfm',
28
+ extends: ['markdown/recommended'],
29
+ },
30
+ ProduckRule.config,
31
+ ProduckRule.excludeGitIgnore(import.meta.url),
32
+ ]);
@@ -135,3 +135,6 @@ dist
135
135
  # Specific ignored / generated.
136
136
  *.ign*
137
137
  *.gen*
138
+
139
+ # @produck/agent-toolkit
140
+ publish-assets/
@@ -40,7 +40,7 @@ Monorepo format (required for multi-package repositories):
40
40
 
41
41
  Example monorepo format:
42
42
 
43
- ```
43
+ ```text
44
44
  foo:
45
45
  [FIX] race condition in auth handler
46
46
  [ADD] <test>: cover edge case for concurrent login
@@ -121,14 +121,14 @@ integration test)`
121
121
 
122
122
  Monorepo examples:
123
123
 
124
- ```
124
+ ```text
125
125
  foo:
126
126
  [FIX] a
127
127
  [FIX] b
128
128
  [FIX] c
129
129
  ```
130
130
 
131
- ```
131
+ ```text
132
132
  core:
133
133
  [ADD] <api>: new user authentication endpoint
134
134
  [REFACTOR] <test>: restructure session management tests
@@ -0,0 +1 @@
1
+ user owned content
@@ -22,8 +22,43 @@
22
22
  "policy": "pinned",
23
23
  "allowLatest": false
24
24
  },
25
+ "eslint": {
26
+ "version": "10.4.0",
27
+ "policy": "pinned",
28
+ "allowLatest": false
29
+ },
30
+ "@eslint/js": {
31
+ "version": "10.0.1",
32
+ "policy": "pinned",
33
+ "allowLatest": false
34
+ },
35
+ "@eslint/json": {
36
+ "version": "1.2.0",
37
+ "policy": "pinned",
38
+ "allowLatest": false
39
+ },
40
+ "@eslint/markdown": {
41
+ "version": "8.0.1",
42
+ "policy": "pinned",
43
+ "allowLatest": false
44
+ },
45
+ "@eslint/config-helpers": {
46
+ "version": "0.6.0",
47
+ "policy": "pinned",
48
+ "allowLatest": false
49
+ },
50
+ "typescript-eslint": {
51
+ "version": "8.59.4",
52
+ "policy": "pinned",
53
+ "allowLatest": false
54
+ },
55
+ "globals": {
56
+ "version": "17.6.0",
57
+ "policy": "pinned",
58
+ "allowLatest": false
59
+ },
25
60
  "@produck/eslint-rules": {
26
- "version": "0.3.5",
61
+ "version": "0.3.6",
27
62
  "policy": "pinned",
28
63
  "allowLatest": false
29
64
  }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "./node_modules/lerna/schemas/lerna-schema.json",
3
+ "command": {
4
+ "version": {
5
+ "commitHooks": false
6
+ },
7
+ "publish": {
8
+ "ignoreScripts": false,
9
+ "message": "[PUBLISH]"
10
+ }
11
+ },
12
+ "useNx": false,
13
+ "version": "independent"
14
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "tabWidth": 2,
5
+ "useTabs": false,
6
+ "trailingComma": "all",
7
+ "bracketSpacing": true,
8
+ "arrowParens": "always",
9
+ "printWidth": 80,
10
+ "endOfLine": "lf"
11
+ }