@outfitter/tooling 0.2.0 → 0.2.2

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.
@@ -0,0 +1,277 @@
1
+ // @bun
2
+ // packages/tooling/src/cli/pre-push.ts
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { join } from "path";
5
+ var COLORS = {
6
+ reset: "\x1B[0m",
7
+ red: "\x1B[31m",
8
+ green: "\x1B[32m",
9
+ yellow: "\x1B[33m",
10
+ blue: "\x1B[34m"
11
+ };
12
+ function log(msg) {
13
+ process.stdout.write(`${msg}
14
+ `);
15
+ }
16
+ function getCurrentBranch() {
17
+ const result = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"]);
18
+ return result.stdout.toString().trim();
19
+ }
20
+ function runGit(args) {
21
+ try {
22
+ const result = Bun.spawnSync(["git", ...args], { stderr: "ignore" });
23
+ if (result.exitCode !== 0) {
24
+ return { ok: false, lines: [] };
25
+ }
26
+ return {
27
+ ok: true,
28
+ lines: result.stdout.toString().split(`
29
+ `).map((line) => line.trim()).filter(Boolean)
30
+ };
31
+ } catch {
32
+ return { ok: false, lines: [] };
33
+ }
34
+ }
35
+ function isRedPhaseBranch(branch) {
36
+ return branch.endsWith("-tests") || branch.endsWith("/tests") || branch.endsWith("_tests");
37
+ }
38
+ function isScaffoldBranch(branch) {
39
+ return branch.endsWith("-scaffold") || branch.endsWith("/scaffold") || branch.endsWith("_scaffold");
40
+ }
41
+ var TEST_PATH_PATTERNS = [
42
+ /(^|\/)__tests__\//,
43
+ /(^|\/)__snapshots__\//,
44
+ /\.(test|spec)\.[cm]?[jt]sx?$/,
45
+ /\.snap$/,
46
+ /(^|\/)(vitest|jest|bun)\.config\.[cm]?[jt]s$/,
47
+ /(^|\/)tsconfig\.test\.json$/,
48
+ /(^|\/)\.env\.test(\.|$)/
49
+ ];
50
+ function isTestOnlyPath(path) {
51
+ const normalized = path.replaceAll("\\", "/");
52
+ return TEST_PATH_PATTERNS.some((pattern) => pattern.test(normalized));
53
+ }
54
+ function areFilesTestOnly(paths) {
55
+ return paths.length > 0 && paths.every((path) => isTestOnlyPath(path));
56
+ }
57
+ function canBypassRedPhaseByChangedFiles(changedFiles) {
58
+ return changedFiles.deterministic && areFilesTestOnly(changedFiles.files);
59
+ }
60
+ function resolveBaseRef() {
61
+ const candidates = [
62
+ "origin/main",
63
+ "main",
64
+ "origin/trunk",
65
+ "trunk",
66
+ "origin/master",
67
+ "master"
68
+ ];
69
+ for (const candidate of candidates) {
70
+ const resolved = runGit(["rev-parse", "--verify", "--quiet", candidate]);
71
+ if (resolved.ok) {
72
+ return candidate;
73
+ }
74
+ }
75
+ return;
76
+ }
77
+ function changedFilesFromRange(range) {
78
+ const result = runGit(["diff", "--name-only", "--diff-filter=d", range]);
79
+ return {
80
+ ok: result.ok,
81
+ files: result.lines
82
+ };
83
+ }
84
+ function getChangedFilesForPush() {
85
+ const upstream = runGit([
86
+ "rev-parse",
87
+ "--abbrev-ref",
88
+ "--symbolic-full-name",
89
+ "@{upstream}"
90
+ ]);
91
+ if (upstream.ok && upstream.lines[0]) {
92
+ const rangeResult = changedFilesFromRange(`${upstream.lines[0]}...HEAD`);
93
+ if (rangeResult.ok) {
94
+ return {
95
+ files: rangeResult.files,
96
+ deterministic: true,
97
+ source: "upstream"
98
+ };
99
+ }
100
+ }
101
+ const baseRef = resolveBaseRef();
102
+ if (baseRef) {
103
+ const rangeResult = changedFilesFromRange(`${baseRef}...HEAD`);
104
+ if (rangeResult.ok) {
105
+ return {
106
+ files: rangeResult.files,
107
+ deterministic: true,
108
+ source: "baseRef"
109
+ };
110
+ }
111
+ }
112
+ return {
113
+ files: [],
114
+ deterministic: false,
115
+ source: "undetermined"
116
+ };
117
+ }
118
+ function maybeSkipForRedPhase(reason, branch) {
119
+ const changedFiles = getChangedFilesForPush();
120
+ if (!changedFiles.deterministic) {
121
+ log(`${COLORS.yellow}RED-phase bypass denied${COLORS.reset}: could not determine full push diff range`);
122
+ log("Running strict verification.");
123
+ log("");
124
+ return false;
125
+ }
126
+ if (!canBypassRedPhaseByChangedFiles(changedFiles)) {
127
+ log(`${COLORS.yellow}RED-phase bypass denied${COLORS.reset}: changed files are not test-only`);
128
+ if (changedFiles.files.length > 0) {
129
+ log(`Changed files (${changedFiles.source}): ${changedFiles.files.join(", ")}`);
130
+ } else {
131
+ log(`No changed files detected in ${changedFiles.source} range. Running strict verification.`);
132
+ }
133
+ log("");
134
+ return false;
135
+ }
136
+ if (reason === "branch") {
137
+ log(`${COLORS.yellow}TDD RED phase${COLORS.reset} detected: ${COLORS.blue}${branch}${COLORS.reset}`);
138
+ } else {
139
+ log(`${COLORS.yellow}Scaffold branch${COLORS.reset} with RED phase branch in context: ${COLORS.blue}${branch}${COLORS.reset}`);
140
+ }
141
+ log(`${COLORS.yellow}Skipping strict verification${COLORS.reset} - changed files are test-only`);
142
+ log(`Diff source: ${changedFiles.source}`);
143
+ log("");
144
+ log("Remember: GREEN phase (implementation) must make these tests pass!");
145
+ return true;
146
+ }
147
+ function hasRedPhaseBranchInContext(currentBranch) {
148
+ let branches = [];
149
+ try {
150
+ const gtResult = Bun.spawnSync(["gt", "ls"], { stderr: "pipe" });
151
+ if (gtResult.exitCode === 0) {
152
+ branches = gtResult.stdout.toString().split(`
153
+ `).map((line) => line.replace(/^[\u2502\u251C\u2514\u2500\u25C9\u25EF ]*/g, "").replace(/ \(.*/, "")).filter(Boolean);
154
+ }
155
+ } catch {}
156
+ if (branches.length === 0) {
157
+ const gitResult = Bun.spawnSync([
158
+ "git",
159
+ "branch",
160
+ "--list",
161
+ "cli/*",
162
+ "types/*",
163
+ "contracts/*"
164
+ ]);
165
+ branches = gitResult.stdout.toString().split(`
166
+ `).map((line) => line.replace(/^[* ]+/, "")).filter(Boolean);
167
+ }
168
+ for (const branch of branches) {
169
+ if (branch === currentBranch)
170
+ continue;
171
+ if (isRedPhaseBranch(branch))
172
+ return true;
173
+ }
174
+ return false;
175
+ }
176
+ function createVerificationPlan(scripts) {
177
+ if (scripts["verify:ci"]) {
178
+ return { ok: true, scripts: ["verify:ci"], source: "verify:ci" };
179
+ }
180
+ const requiredScripts = ["typecheck", "build", "test"];
181
+ const missingRequired = requiredScripts.filter((name) => !scripts[name]);
182
+ const checkOrLint = scripts["check"] ? "check" : scripts["lint"] ? "lint" : undefined;
183
+ if (!checkOrLint || missingRequired.length > 0) {
184
+ const missing = checkOrLint ? missingRequired : [...missingRequired, "check|lint"];
185
+ return {
186
+ ok: false,
187
+ error: `Missing required scripts for strict pre-push verification: ${missing.join(", ")}`
188
+ };
189
+ }
190
+ return {
191
+ ok: true,
192
+ scripts: ["typecheck", checkOrLint, "build", "test"],
193
+ source: "fallback"
194
+ };
195
+ }
196
+ function readPackageScripts(cwd = process.cwd()) {
197
+ const packageJsonPath = join(cwd, "package.json");
198
+ if (!existsSync(packageJsonPath)) {
199
+ return {};
200
+ }
201
+ try {
202
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
203
+ const scripts = parsed.scripts ?? {};
204
+ const normalized = {};
205
+ for (const [name, value] of Object.entries(scripts)) {
206
+ if (typeof value === "string") {
207
+ normalized[name] = value;
208
+ }
209
+ }
210
+ return normalized;
211
+ } catch {
212
+ return {};
213
+ }
214
+ }
215
+ function runScript(scriptName) {
216
+ log("");
217
+ log(`Running: ${COLORS.blue}bun run ${scriptName}${COLORS.reset}`);
218
+ const result = Bun.spawnSync(["bun", "run", scriptName], {
219
+ stdio: ["inherit", "inherit", "inherit"]
220
+ });
221
+ return result.exitCode === 0;
222
+ }
223
+ async function runPrePush(options = {}) {
224
+ log(`${COLORS.blue}Pre-push verify${COLORS.reset} (TDD-aware)`);
225
+ log("");
226
+ const branch = getCurrentBranch();
227
+ if (isRedPhaseBranch(branch)) {
228
+ if (maybeSkipForRedPhase("branch", branch)) {
229
+ process.exit(0);
230
+ }
231
+ }
232
+ if (isScaffoldBranch(branch)) {
233
+ if (hasRedPhaseBranchInContext(branch)) {
234
+ if (maybeSkipForRedPhase("context", branch)) {
235
+ process.exit(0);
236
+ }
237
+ }
238
+ }
239
+ if (options.force) {
240
+ log(`${COLORS.yellow}Force flag set${COLORS.reset} - skipping strict verification`);
241
+ process.exit(0);
242
+ }
243
+ const plan = createVerificationPlan(readPackageScripts());
244
+ if (!plan.ok) {
245
+ log(`${COLORS.red}Strict pre-push verification is not configured${COLORS.reset}`);
246
+ log(plan.error);
247
+ log("");
248
+ log("Add one of:");
249
+ log(" - verify:ci");
250
+ log(" - typecheck + (check or lint) + build + test");
251
+ process.exit(1);
252
+ }
253
+ log(`Running strict verification for branch: ${COLORS.blue}${branch}${COLORS.reset}`);
254
+ if (plan.source === "verify:ci") {
255
+ log("Using `verify:ci` script.");
256
+ } else {
257
+ log(`Using fallback scripts: ${plan.scripts.join(" -> ")}`);
258
+ }
259
+ for (const scriptName of plan.scripts) {
260
+ if (runScript(scriptName)) {
261
+ continue;
262
+ }
263
+ log("");
264
+ log(`${COLORS.red}Verification failed${COLORS.reset} on script: ${scriptName}`);
265
+ log("");
266
+ log("If this is intentional TDD RED phase work, name your branch:");
267
+ log(" - feature-tests");
268
+ log(" - feature/tests");
269
+ log(" - feature_tests");
270
+ process.exit(1);
271
+ }
272
+ log("");
273
+ log(`${COLORS.green}Strict verification passed${COLORS.reset}`);
274
+ process.exit(0);
275
+ }
276
+
277
+ export { isRedPhaseBranch, isScaffoldBranch, isTestOnlyPath, areFilesTestOnly, canBypassRedPhaseByChangedFiles, createVerificationPlan, runPrePush };
@@ -9,7 +9,8 @@ function buildFixCommand(options) {
9
9
  }
10
10
  async function runFix(paths = []) {
11
11
  const cmd = buildFixCommand({ paths });
12
- console.log(`Running: bun x ${cmd.join(" ")}`);
12
+ process.stdout.write(`Running: bun x ${cmd.join(" ")}
13
+ `);
13
14
  const proc = Bun.spawn(["bun", "x", ...cmd], {
14
15
  stdio: ["inherit", "inherit", "inherit"]
15
16
  });
@@ -10,16 +10,20 @@ var COLORS = {
10
10
  blue: "\x1B[34m"
11
11
  };
12
12
  function log(msg) {
13
- console.log(msg);
13
+ process.stdout.write(`${msg}
14
+ `);
14
15
  }
15
16
  function info(msg) {
16
- console.log(`${COLORS.blue}\u25B8${COLORS.reset} ${msg}`);
17
+ process.stdout.write(`${COLORS.blue}\u25B8${COLORS.reset} ${msg}
18
+ `);
17
19
  }
18
20
  function success(msg) {
19
- console.log(`${COLORS.green}\u2713${COLORS.reset} ${msg}`);
21
+ process.stdout.write(`${COLORS.green}\u2713${COLORS.reset} ${msg}
22
+ `);
20
23
  }
21
24
  function warn(msg) {
22
- console.log(`${COLORS.yellow}!${COLORS.reset} ${msg}`);
25
+ process.stdout.write(`${COLORS.yellow}!${COLORS.reset} ${msg}
26
+ `);
23
27
  }
24
28
  async function fetchLatestVersion() {
25
29
  const response = await fetch("https://api.github.com/repos/oven-sh/bun/releases/latest");
@@ -91,13 +95,13 @@ async function runUpgradeBun(targetVersion, options = {}) {
91
95
  info("Updating engines.bun...");
92
96
  for (const file of packageFiles) {
93
97
  if (updateEnginesBun(file, version)) {
94
- log(` ${file.replace(cwd + "/", "")}`);
98
+ log(` ${file.replace(`${cwd}/`, "")}`);
95
99
  }
96
100
  }
97
101
  info("Updating @types/bun...");
98
102
  for (const file of packageFiles) {
99
103
  if (updateTypesBun(file, version)) {
100
- log(` ${file.replace(cwd + "/", "")}`);
104
+ log(` ${file.replace(`${cwd}/`, "")}`);
101
105
  }
102
106
  }
103
107
  if (options.install !== false) {
@@ -29,7 +29,15 @@ function detectFrameworks(pkg) {
29
29
  return ["--frameworks", ...detected];
30
30
  }
31
31
  function buildUltraciteCommand(options) {
32
- const cmd = ["ultracite", "init", "--linter", "biome", "--pm", "bun", "--quiet"];
32
+ const cmd = [
33
+ "ultracite",
34
+ "init",
35
+ "--linter",
36
+ "biome",
37
+ "--pm",
38
+ "bun",
39
+ "--quiet"
40
+ ];
33
41
  if (options.frameworks && options.frameworks.length > 0) {
34
42
  cmd.push("--frameworks", ...options.frameworks);
35
43
  }
@@ -39,14 +47,16 @@ async function runInit(cwd = process.cwd()) {
39
47
  const pkgPath = `${cwd}/package.json`;
40
48
  const pkgFile = Bun.file(pkgPath);
41
49
  if (!await pkgFile.exists()) {
42
- console.error("No package.json found in current directory");
50
+ process.stderr.write(`No package.json found in current directory
51
+ `);
43
52
  process.exit(1);
44
53
  }
45
54
  const pkg = await pkgFile.json();
46
55
  const frameworkFlags = detectFrameworks(pkg);
47
56
  const frameworks = frameworkFlags.length > 0 ? frameworkFlags.slice(1) : [];
48
57
  const cmd = buildUltraciteCommand({ frameworks });
49
- console.log(`Running: bun x ${cmd.join(" ")}`);
58
+ process.stdout.write(`Running: bun x ${cmd.join(" ")}
59
+ `);
50
60
  const proc = Bun.spawn(["bun", "x", ...cmd], {
51
61
  cwd,
52
62
  stdio: ["inherit", "inherit", "inherit"]
package/lefthook.yml CHANGED
@@ -20,11 +20,9 @@ pre-commit:
20
20
  pre-push:
21
21
  parallel: false
22
22
  commands:
23
- build:
24
- run: bun run build
25
-
26
- test:
27
- # TDD-aware: skips tests on RED phase branches (*-tests, */tests, *_tests)
28
- # Override with `run: bun run test` if you don't want TDD support
29
- # Requires: @outfitter/tooling must be a devDependency in your project
23
+ verify:
24
+ # TDD-aware: skips strict verification on explicit RED phase branches
25
+ # (*-tests, */tests, *_tests). Otherwise runs verify:ci (or a strict
26
+ # fallback: typecheck/check-or-lint/build/test).
27
+ # Requires: @outfitter/tooling as a dev dependency.
30
28
  run: bunx @outfitter/tooling pre-push
package/package.json CHANGED
@@ -1,116 +1,123 @@
1
1
  {
2
- "name": "@outfitter/tooling",
3
- "description": "Dev tooling configuration presets for Outfitter projects (biome, typescript, lefthook, markdownlint)",
4
- "version": "0.2.0",
5
- "type": "module",
6
- "files": [
7
- "dist",
8
- "registry",
9
- "biome.json",
10
- "tsconfig.preset.json",
11
- "tsconfig.preset.bun.json",
12
- "lefthook.yml",
13
- ".markdownlint-cli2.jsonc"
14
- ],
15
- "module": "./dist/index.js",
16
- "types": "./dist/index.d.ts",
17
- "exports": {
18
- ".": {
19
- "import": {
20
- "types": "./dist/index.d.ts",
21
- "default": "./dist/index.js"
22
- }
23
- },
24
- "./registry": {
25
- "import": {
26
- "types": "./dist/index.d.ts",
27
- "default": "./dist/index.js"
28
- }
29
- },
30
- "./cli/init": {
31
- "import": {
32
- "types": "./dist/cli/init.d.ts",
33
- "default": "./dist/cli/init.js"
34
- }
35
- },
36
- "./cli/fix": {
37
- "import": {
38
- "types": "./dist/cli/fix.d.ts",
39
- "default": "./dist/cli/fix.js"
40
- }
41
- },
42
- "./cli/check": {
43
- "import": {
44
- "types": "./dist/cli/check.d.ts",
45
- "default": "./dist/cli/check.js"
46
- }
47
- },
48
- "./package.json": "./package.json",
49
- "./biome.json": "./biome.json",
50
- "./tsconfig.preset.json": "./tsconfig.preset.json",
51
- "./tsconfig.preset.bun.json": "./tsconfig.preset.bun.json",
52
- "./lefthook.yml": "./lefthook.yml",
53
- "./.markdownlint-cli2.jsonc": "./.markdownlint-cli2.jsonc"
54
- },
55
- "bin": {
56
- "tooling": "./dist/cli/index.js"
57
- },
58
- "sideEffects": false,
59
- "scripts": {
60
- "build:registry": "bun run src/registry/build.ts",
61
- "prebuild": "bun run build:registry",
62
- "build": "bunup --filter @outfitter/tooling",
63
- "lint": "biome lint ./src",
64
- "lint:fix": "biome lint --write ./src",
65
- "test": "bun test",
66
- "typecheck": "tsc --noEmit",
67
- "clean": "rm -rf dist registry"
68
- },
69
- "dependencies": {
70
- "commander": "^14.0.2",
71
- "zod": "^3.25.17"
72
- },
73
- "devDependencies": {
74
- "@types/bun": "^1.3.7",
75
- "typescript": "^5.9.3",
76
- "yaml": "^2.8.2"
77
- },
78
- "peerDependencies": {
79
- "ultracite": "^7.0.0",
80
- "lefthook": "^2.0.0",
81
- "markdownlint-cli2": ">=0.17.0"
82
- },
83
- "peerDependenciesMeta": {
84
- "ultracite": {
85
- "optional": true
86
- },
87
- "lefthook": {
88
- "optional": true
89
- },
90
- "markdownlint-cli2": {
91
- "optional": true
92
- }
93
- },
94
- "engines": {
95
- "bun": ">=1.3.7"
96
- },
97
- "keywords": [
98
- "outfitter",
99
- "tooling",
100
- "biome",
101
- "typescript",
102
- "lefthook",
103
- "markdownlint",
104
- "config",
105
- "presets"
106
- ],
107
- "license": "MIT",
108
- "repository": {
109
- "type": "git",
110
- "url": "https://github.com/outfitter-dev/outfitter.git",
111
- "directory": "packages/tooling"
112
- },
113
- "publishConfig": {
114
- "access": "public"
115
- }
2
+ "name": "@outfitter/tooling",
3
+ "description": "Dev tooling configuration presets for Outfitter projects (biome, typescript, lefthook, markdownlint)",
4
+ "version": "0.2.2",
5
+ "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "registry",
9
+ "biome.json",
10
+ "tsconfig.preset.json",
11
+ "tsconfig.preset.bun.json",
12
+ "lefthook.yml",
13
+ ".markdownlint-cli2.jsonc"
14
+ ],
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "./registry": {
25
+ "import": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "./cli/init": {
31
+ "import": {
32
+ "types": "./dist/cli/init.d.ts",
33
+ "default": "./dist/cli/init.js"
34
+ }
35
+ },
36
+ "./cli/fix": {
37
+ "import": {
38
+ "types": "./dist/cli/fix.d.ts",
39
+ "default": "./dist/cli/fix.js"
40
+ }
41
+ },
42
+ "./cli/check": {
43
+ "import": {
44
+ "types": "./dist/cli/check.d.ts",
45
+ "default": "./dist/cli/check.js"
46
+ }
47
+ },
48
+ "./package.json": "./package.json",
49
+ "./biome": "./biome.json",
50
+ "./biome.json": "./biome.json",
51
+ "./tsconfig": "./tsconfig.preset.json",
52
+ "./tsconfig.preset.json": "./tsconfig.preset.json",
53
+ "./tsconfig-bun": "./tsconfig.preset.bun.json",
54
+ "./tsconfig.preset.bun.json": "./tsconfig.preset.bun.json",
55
+ "./lefthook": "./lefthook.yml",
56
+ "./lefthook.yml": "./lefthook.yml",
57
+ "./.markdownlint-cli2": "./.markdownlint-cli2.jsonc",
58
+ "./.markdownlint-cli2.jsonc": "./.markdownlint-cli2.jsonc"
59
+ },
60
+ "bin": {
61
+ "tooling": "./dist/cli/index.js"
62
+ },
63
+ "sideEffects": false,
64
+ "scripts": {
65
+ "build:registry": "bun run src/registry/build.ts",
66
+ "sync:exports": "bun run scripts/sync-exports.ts",
67
+ "prebuild": "bun run build:registry && bun run sync:exports",
68
+ "build": "bunup --filter @outfitter/tooling",
69
+ "prepack": "bun run sync:exports",
70
+ "lint": "biome lint ./src",
71
+ "lint:fix": "biome lint --write ./src",
72
+ "test": "bun test",
73
+ "typecheck": "tsc --noEmit",
74
+ "clean": "rm -rf dist registry"
75
+ },
76
+ "dependencies": {
77
+ "commander": "^14.0.2",
78
+ "zod": "^3.25.17"
79
+ },
80
+ "devDependencies": {
81
+ "@types/bun": "^1.3.7",
82
+ "typescript": "^5.9.3",
83
+ "yaml": "^2.8.2"
84
+ },
85
+ "peerDependencies": {
86
+ "ultracite": "^7.0.0",
87
+ "lefthook": "^2.0.0",
88
+ "markdownlint-cli2": ">=0.17.0"
89
+ },
90
+ "peerDependenciesMeta": {
91
+ "ultracite": {
92
+ "optional": true
93
+ },
94
+ "lefthook": {
95
+ "optional": true
96
+ },
97
+ "markdownlint-cli2": {
98
+ "optional": true
99
+ }
100
+ },
101
+ "engines": {
102
+ "bun": ">=1.3.7"
103
+ },
104
+ "keywords": [
105
+ "outfitter",
106
+ "tooling",
107
+ "biome",
108
+ "typescript",
109
+ "lefthook",
110
+ "markdownlint",
111
+ "config",
112
+ "presets"
113
+ ],
114
+ "license": "MIT",
115
+ "repository": {
116
+ "type": "git",
117
+ "url": "https://github.com/outfitter-dev/outfitter.git",
118
+ "directory": "packages/tooling"
119
+ },
120
+ "publishConfig": {
121
+ "access": "public"
122
+ }
116
123
  }