@bleedingdev/modern-js-create 3.2.0-ultramodern.10 → 3.2.0-ultramodern.12

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/package.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
24
- "version": "3.2.0-ultramodern.10",
24
+ "version": "3.2.0-ultramodern.12",
25
25
  "types": "./dist/types/index.d.ts",
26
26
  "main": "./dist/index.js",
27
27
  "bin": {
@@ -41,7 +41,7 @@
41
41
  "@types/node": "^25.8.0",
42
42
  "@typescript/native-preview": "7.0.0-dev.20260516.1",
43
43
  "tsx": "^4.22.0",
44
- "@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.2.0-ultramodern.10"
44
+ "@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.2.0-ultramodern.12"
45
45
  },
46
46
  "publishConfig": {
47
47
  "registry": "https://registry.npmjs.org/",
@@ -54,6 +54,6 @@
54
54
  "start": "node ./dist/index.js"
55
55
  },
56
56
  "ultramodern": {
57
- "frameworkVersion": "3.2.0-ultramodern.10"
57
+ "frameworkVersion": "3.2.0-ultramodern.12"
58
58
  }
59
59
  }
@@ -7,8 +7,13 @@ This project is generated for Codex-first UltraModern.js work.
7
7
  - `pnpm lint` runs Oxlint with the Ultracite preset.
8
8
  - `pnpm format` runs oxfmt.
9
9
  - `pnpm typecheck` runs effect-tsgo as the TypeScript checker.
10
+ - `pnpm i18n:check` rejects hardcoded user-visible JSX text.
10
11
  - `pnpm ultramodern:check` verifies the generated contract.
11
12
 
13
+ ## Internationalization
14
+
15
+ Runtime i18n is enabled by default. Agents must put user-visible UI copy in `config/public/locales/<lang>/translation.json` and render it through `react-i18next` or `@modern-js/plugin-i18n/runtime`. Do not add hardcoded JSX text, `aria-label`, `title`, `alt`, or `placeholder` strings unless the value is a non-translatable technical token.
16
+
12
17
  ## Private Skills
13
18
 
14
19
  Private orchestration skills are not vendored into this template. If you are authorized for `TechsioCZ/skills`, run:
@@ -0,0 +1,39 @@
1
+ {
2
+ "home": {
3
+ "bff": {
4
+ "response": "Odpoved Effect HttpApi:"
5
+ },
6
+ "cards": {
7
+ "bff": {
8
+ "body": "Pouzivej Effect jako hlavni BFF cestu, Hono nech jako explicitni zalozni volbu.",
9
+ "title": "BFF + Effect"
10
+ },
11
+ "config": {
12
+ "body": "Upravuj vygenerovane vychozi hodnoty v modern.config.ts.",
13
+ "title": "Konfigurace presetUltramodern"
14
+ },
15
+ "gates": {
16
+ "body": "Starter obsahuje PR workflow pro ultramodern:check a build.",
17
+ "title": "Ultramodern kontroly"
18
+ },
19
+ "guide": {
20
+ "body": "Projdi si verejny preset pripraveny pro MV, TanStack a Effect.",
21
+ "title": "UltraModern.js pruvodce"
22
+ }
23
+ },
24
+ "description": {
25
+ "afterConfig": ", udrzuj",
26
+ "afterPreset": "profil. Zacni v",
27
+ "end": "zelene a lad vygenerovany preset jen tam, kde aplikace potrebuje mekci cestu.",
28
+ "intro": "Tento starter prinasi verejny"
29
+ },
30
+ "language": {
31
+ "cs": "Cestina",
32
+ "en": "Anglictina",
33
+ "switcher": "Jazyk"
34
+ },
35
+ "logoAlt": "Logo UltraModern.js",
36
+ "name": "presetUltramodern",
37
+ "title": "UltraModern.js 3.0"
38
+ }
39
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "home": {
3
+ "bff": {
4
+ "response": "Effect HttpApi response:"
5
+ },
6
+ "cards": {
7
+ "bff": {
8
+ "body": "Keep Effect as the preferred BFF lane while Hono stays an explicit fallback.",
9
+ "title": "BFF + Effect"
10
+ },
11
+ "config": {
12
+ "body": "Tune the generated defaults in modern.config.ts.",
13
+ "title": "Configure presetUltramodern"
14
+ },
15
+ "gates": {
16
+ "body": "The starter includes a PR workflow for ultramodern:check and build.",
17
+ "title": "Ultramodern Gates"
18
+ },
19
+ "guide": {
20
+ "body": "Review the MV-first, TanStack-ready, Effect-ready public preset.",
21
+ "title": "UltraModern.js Guide"
22
+ }
23
+ },
24
+ "description": {
25
+ "afterConfig": ", keep",
26
+ "afterPreset": "profile. Start in",
27
+ "end": "green, and tune the generated preset only where your app needs a softer lane.",
28
+ "intro": "This starter ships the public"
29
+ },
30
+ "language": {
31
+ "cs": "Czech",
32
+ "en": "English",
33
+ "switcher": "Language"
34
+ },
35
+ "logoAlt": "UltraModern.js Logo",
36
+ "name": "presetUltramodern",
37
+ "title": "UltraModern.js 3.0"
38
+ }
39
+ }
@@ -2,7 +2,8 @@
2
2
  import { appTools, defineConfig, presetUltramodern } from '@modern-js/app-tools';
3
3
  import path from 'node:path';
4
4
  {{#if enableBff}}import { bffPlugin } from '@modern-js/plugin-bff';
5
- {{/if}}{{#if isTanstackRouter}}import { tanstackRouterPlugin } from '@modern-js/plugin-tanstack';
5
+ {{/if}}import { i18nPlugin } from '@modern-js/plugin-i18n';
6
+ {{#if isTanstackRouter}}import { tanstackRouterPlugin } from '@modern-js/plugin-tanstack';
6
7
  {{/if}}
7
8
  const appId = process.env['MODERN_BASELINE_APP_ID'] || path.basename(process.cwd());
8
9
  const enableModuleFederationSSR = process.env['MODERN_BASELINE_ENABLE_MF_SSR'] !== 'false';
@@ -27,6 +28,12 @@ export default defineConfig(
27
28
 
28
29
  {{/if}} plugins: [
29
30
  appTools(),
31
+ i18nPlugin({
32
+ localeDetection: {
33
+ fallbackLanguage: 'en',
34
+ languages: ['en', 'cs'],
35
+ },
36
+ }),
30
37
  {{#if isTanstackRouter}}
31
38
  tanstackRouterPlugin(),
32
39
  {{/if}}{{#if enableBff}}
@@ -8,9 +8,10 @@
8
8
  "build": "modern build",
9
9
  "serve": "modern serve",
10
10
  "typecheck": "node -e \"const fs = require('node:fs'); const { execFileSync, spawnSync } = require('node:child_process'); const bin = execFileSync('effect-tsgo', ['get-exe-path'], { encoding: 'utf8' }).trim(); if (process.platform !== 'win32') fs.chmodSync(bin, 0o755); const result = spawnSync(bin, ['--noEmit', '-p', 'tsconfig.json'], { stdio: 'inherit' }); process.exit(result.status ?? 1);\"",
11
+ "i18n:check": "node ./scripts/check-i18n-strings.mjs",
11
12
  "skills:install": "node ./scripts/bootstrap-agent-skills.mjs",
12
13
  "skills:check": "node ./scripts/bootstrap-agent-skills.mjs --check",
13
- "ultramodern:check": "{{#unless isSubproject}}pnpm format:check && pnpm lint && {{/unless}}pnpm typecheck{{#unless isSubproject}} && pnpm skills:check{{/unless}} && node ./scripts/validate-ultramodern.mjs"{{#unless isSubproject}},
14
+ "ultramodern:check": "{{#unless isSubproject}}pnpm format:check && pnpm lint && {{/unless}}pnpm typecheck && pnpm i18n:check{{#unless isSubproject}} && pnpm skills:check{{/unless}} && node ./scripts/validate-ultramodern.mjs"{{#unless isSubproject}},
14
15
  "format": "oxfmt .",
15
16
  "format:check": "oxfmt --check .",
16
17
  "lint": "oxlint .",
@@ -18,12 +19,16 @@
18
19
  "prepare": "simple-git-hooks"{{/unless}}
19
20
  },
20
21
  "dependencies": {
22
+ "@modern-js/plugin-i18n": "{{pluginI18nVersion}}",
21
23
  {{#if isTanstackRouter}} "@modern-js/plugin-tanstack": "{{pluginTanstackVersion}}",
22
- {{/if}} "@modern-js/runtime": "{{runtimeVersion}}",
24
+ {{/if}}
25
+ "@modern-js/runtime": "{{runtimeVersion}}",
23
26
  {{#if isTanstackRouter}} "@tanstack/react-router": "1.170.1",
24
27
  {{/if}}
28
+ "i18next": "26.2.0",
25
29
  "react": "^19.2.3",
26
- "react-dom": "^19.2.0"
30
+ "react-dom": "^19.2.0",
31
+ "react-i18next": "17.0.8"
27
32
  },
28
33
  "devDependencies": {
29
34
  "@effect/tsgo": "0.7.3",
@@ -0,0 +1,83 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const root = process.cwd();
5
+ const scanRoots = ['src'].map((scanRoot) => path.join(root, scanRoot));
6
+ const ignoredDirectories = new Set(['.modern', '.modernjs', 'dist', 'node_modules']);
7
+ const visibleAttributePattern =
8
+ /\s(?:aria-label|alt|placeholder|title)=["']([^"']*[A-Za-z][^"']*)["']/gu;
9
+ const jsxTextPattern = />([^<>{}]*[A-Za-z][^<>{}]*)</gu;
10
+
11
+ const collectFiles = (directory) => {
12
+ if (!fs.existsSync(directory)) {
13
+ return [];
14
+ }
15
+
16
+ const files = [];
17
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
18
+ if (entry.isDirectory()) {
19
+ if (!ignoredDirectories.has(entry.name)) {
20
+ files.push(...collectFiles(path.join(directory, entry.name)));
21
+ }
22
+ continue;
23
+ }
24
+
25
+ if (entry.isFile() && /\.(jsx|tsx)$/u.test(entry.name) && !entry.name.endsWith('.d.ts')) {
26
+ files.push(path.join(directory, entry.name));
27
+ }
28
+ }
29
+ return files;
30
+ };
31
+
32
+ const lineNumberForIndex = (content, index) => content.slice(0, index).split('\n').length;
33
+ const isIgnoredLine = (content, index) => {
34
+ const lineStart = content.lastIndexOf('\n', index) + 1;
35
+ const lineEnd = content.indexOf('\n', index);
36
+ const currentLineEnd = lineEnd === -1 ? content.length : lineEnd;
37
+ const previousLineStart = content.lastIndexOf('\n', Math.max(0, lineStart - 2)) + 1;
38
+ const nextLineEnd = content.indexOf('\n', currentLineEnd + 1);
39
+ const context = content.slice(
40
+ previousLineStart,
41
+ nextLineEnd === -1 ? content.length : nextLineEnd,
42
+ );
43
+ return /i18n-ignore/u.test(context);
44
+ };
45
+
46
+ const violations = [];
47
+ for (const filePath of scanRoots.flatMap(collectFiles)) {
48
+ const content = fs.readFileSync(filePath, 'utf-8');
49
+ for (const match of content.matchAll(visibleAttributePattern)) {
50
+ if (!isIgnoredLine(content, match.index ?? 0)) {
51
+ violations.push({
52
+ filePath,
53
+ line: lineNumberForIndex(content, match.index ?? 0),
54
+ text: match[1].trim(),
55
+ });
56
+ }
57
+ }
58
+
59
+ for (const match of content.matchAll(jsxTextPattern)) {
60
+ const text = match[1].replaceAll(/\s+/gu, ' ').trim();
61
+ if (text && !isIgnoredLine(content, match.index ?? 0)) {
62
+ violations.push({
63
+ filePath,
64
+ line: lineNumberForIndex(content, match.index ?? 0),
65
+ text,
66
+ });
67
+ }
68
+ }
69
+ }
70
+
71
+ if (violations.length > 0) {
72
+ console.error('Hardcoded user-visible JSX strings found. Move copy to locale JSON files.');
73
+ for (const violation of violations) {
74
+ console.error(
75
+ `${path.relative(root, violation.filePath)}:${violation.line} ${JSON.stringify(
76
+ violation.text,
77
+ )}`,
78
+ );
79
+ }
80
+ process.exit(1);
81
+ }
82
+
83
+ console.log('No hardcoded user-visible JSX strings found.');
@@ -16,6 +16,7 @@ const requiredTokens = [
16
16
  'enableModuleFederationSSR',
17
17
  'enableBffRequestId',
18
18
  'enableTelemetryExporters',
19
+ 'i18nPlugin(',
19
20
  ];
20
21
  const missing = requiredTokens.filter((token) => !content.includes(token));
21
22
 
@@ -51,6 +52,9 @@ const requiredPaths = [
51
52
  'oxlint.config.ts',
52
53
  'oxfmt.config.ts',
53
54
  'scripts/bootstrap-agent-skills.mjs',
55
+ 'scripts/check-i18n-strings.mjs',
56
+ 'config/public/locales/en/translation.json',
57
+ 'config/public/locales/cs/translation.json',
54
58
  ];
55
59
  const manifestErrors = [];
56
60
 
@@ -118,6 +122,7 @@ const skillsLock = JSON.parse(
118
122
  const requiredScripts = {
119
123
  format: 'oxfmt .',
120
124
  'format:check': 'oxfmt --check .',
125
+ 'i18n:check': 'node ./scripts/check-i18n-strings.mjs',
121
126
  lint: 'oxlint .',
122
127
  'lint:fix': 'oxlint . --fix',
123
128
  'skills:check': 'node ./scripts/bootstrap-agent-skills.mjs --check',
@@ -139,6 +144,13 @@ if (
139
144
  process.exit(1);
140
145
  }
141
146
 
147
+ for (const dependency of ['@modern-js/plugin-i18n', 'i18next', 'react-i18next']) {
148
+ if (!packageJson.dependencies?.[dependency]) {
149
+ console.error(`Missing dependency: ${dependency}`);
150
+ process.exit(1);
151
+ }
152
+ }
153
+
142
154
  for (const dependency of [
143
155
  '@effect/tsgo',
144
156
  '@typescript/native-preview',
@@ -1,7 +1,23 @@
1
1
  import { defineRuntimeConfig } from '@modern-js/runtime';
2
+ import { createInstance } from 'i18next';
3
+
4
+ const i18nInstance = createInstance();
2
5
 
3
6
  export default defineRuntimeConfig({
7
+ i18n: {
8
+ i18nInstance,
9
+ initOptions: {
10
+ defaultNS: 'translation',
11
+ fallbackLng: 'en',
12
+ interpolation: {
13
+ escapeValue: false,
14
+ },
15
+ ns: ['translation'],
16
+ supportedLngs: ['en', 'cs'],
17
+ },
18
+ },
4
19
  {{#if isTanstackRouter}} router: {
5
20
  framework: 'tanstack',
6
- },{{/if}}
21
+ },
22
+ {{/if~}}
7
23
  });
@@ -1,11 +1,19 @@
1
1
  import { Helmet } from '@modern-js/runtime/head';
2
+ import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
2
3
  {{#if useEffectBff}}import effectBff from '@api/effect/index';
3
4
  import { Effect } from '@modern-js/plugin-bff/effect-client';
4
5
  import { useEffect, useState } from 'react';
5
6
  {{/if}}
7
+ import { useTranslation } from 'react-i18next';
6
8
  import './index.css';
7
9
 
8
10
  const Index = () => {
11
+ const { t } = useTranslation();
12
+ const { changeLanguage, language } = useModernI18n();
13
+ const languageOptions = [
14
+ { code: 'en', label: t('home.language.en') },
15
+ { code: 'cs', label: t('home.language.cs') },
16
+ ];
9
17
  {{#if useEffectBff}} const [effectMessage, setEffectMessage] = useState('loading...');
10
18
 
11
19
  useEffect(() => {
@@ -36,25 +44,41 @@ const Index = () => {
36
44
  />
37
45
  </Helmet>
38
46
  <main>
47
+ <nav className="language-switcher" aria-label={t('home.language.switcher')}>
48
+ {languageOptions.map((option) => (
49
+ <button
50
+ disabled={language === option.code}
51
+ key={option.code}
52
+ onClick={() => void changeLanguage(option.code)}
53
+ type="button"
54
+ >
55
+ {option.label}
56
+ </button>
57
+ ))}
58
+ </nav>
39
59
  <div className="title">
40
- UltraModern.js 3.0
60
+ {t('home.title')}
41
61
  <img
62
+ alt={t('home.logoAlt')}
42
63
  className="logo"
43
64
  src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/modern-js-logo.svg"
44
- alt="UltraModern.js Logo"
45
65
  />
46
- <p className="name">presetUltramodern</p>
66
+ <p className="name">{t('home.name')}</p>
47
67
  </div>
48
68
  <p className="description{{#if enableTailwind}} text-emerald-700 font-semibold{{/if}}">
49
- This starter ships the public <code className="code">presetUltramodern(...)</code>{' '}
50
- profile. Start in
51
- <code className="code">modern.config.ts</code>, keep
52
- <code className="code">pnpm run ultramodern:check</code> green, and tune the generated
53
- preset only where your app needs a softer lane.
69
+ {t('home.description.intro')} <code className="code">presetUltramodern(...)</code>{' '}
70
+ {/* i18n-ignore technical token */}
71
+ {t('home.description.afterPreset')}
72
+ <code className="code">modern.config.ts</code>
73
+ {/* i18n-ignore technical token */}
74
+ {t('home.description.afterConfig')}
75
+ <code className="code">pnpm run ultramodern:check</code>
76
+ {/* i18n-ignore technical token */}
77
+ {t('home.description.end')}
54
78
  </p>
55
79
  {{#if useEffectBff}}
56
80
  <p className="description effect-message{{#if enableTailwind}} text-emerald-700 font-semibold{{/if}}">
57
- Effect HttpApi response: <code className="code">{effectMessage}</code>
81
+ {t('home.bff.response')} <code className="code">{effectMessage}</code>
58
82
  </p>
59
83
  {{/if}}
60
84
  <div className="grid">
@@ -65,14 +89,14 @@ const Index = () => {
65
89
  className="card"
66
90
  >
67
91
  <h2>
68
- UltraModern.js Guide
92
+ {t('home.cards.guide.title')}
69
93
  <img
94
+ alt=""
70
95
  className="arrow-right"
71
96
  src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
72
- alt="Guide"
73
97
  />
74
98
  </h2>
75
- <p>Review the MV-first, TanStack-ready, Effect-ready public preset.</p>
99
+ <p>{t('home.cards.guide.body')}</p>
76
100
  </a>
77
101
  <a
78
102
  href="https://bleedingdev.github.io/ultramodern.js/configure/app/usage.html"
@@ -81,16 +105,14 @@ const Index = () => {
81
105
  rel="noreferrer"
82
106
  >
83
107
  <h2>
84
- Configure presetUltramodern
108
+ {t('home.cards.config.title')}
85
109
  <img
110
+ alt=""
86
111
  className="arrow-right"
87
112
  src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
88
- alt="Tutorials"
89
113
  />
90
114
  </h2>
91
- <p>
92
- Tune the generated defaults in <code className="code">modern.config.ts</code>.
93
- </p>
115
+ <p>{t('home.cards.config.body')}</p>
94
116
  </a>
95
117
  <a
96
118
  href="https://github.com/BleedingDev/ultramodern.js/blob/main-ultramodern/packages/toolkit/create/template/.github/workflows/ultramodern-gates.yml.handlebars"
@@ -99,17 +121,14 @@ const Index = () => {
99
121
  rel="noreferrer"
100
122
  >
101
123
  <h2>
102
- Ultramodern Gates
124
+ {t('home.cards.gates.title')}
103
125
  <img
126
+ alt=""
104
127
  className="arrow-right"
105
128
  src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
106
- alt="Config"
107
129
  />
108
130
  </h2>
109
- <p>
110
- The starter includes a PR workflow for <code className="code">ultramodern:check</code>{' '}
111
- and build.
112
- </p>
131
+ <p>{t('home.cards.gates.body')}</p>
113
132
  </a>
114
133
  <a
115
134
  href="https://bleedingdev.github.io/ultramodern.js/configure/app/bff/effect.html"
@@ -118,14 +137,14 @@ const Index = () => {
118
137
  className="card"
119
138
  >
120
139
  <h2>
121
- BFF + Effect
140
+ {t('home.cards.bff.title')}
122
141
  <img
142
+ alt=""
123
143
  className="arrow-right"
124
144
  src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
125
- alt="Github"
126
145
  />
127
146
  </h2>
128
- <p>Keep Effect as the preferred BFF lane while Hono stays an explicit fallback.</p>
147
+ <p>{t('home.cards.bff.body')}</p>
129
148
  </a>
130
149
  </div>
131
150
  </main>
@@ -7,7 +7,12 @@ This workspace is generated as an agent-ready UltraModern.js SuperApp. Agents sh
7
7
  - `pnpm lint` runs Oxlint with the Ultracite preset.
8
8
  - `pnpm format` runs oxfmt.
9
9
  - `pnpm typecheck` runs effect-tsgo as the TypeScript checker.
10
- - `pnpm check` runs formatting, linting, effect-tsgo, private-skill availability checks, and the generated workspace contract.
10
+ - `pnpm i18n:check` rejects hardcoded user-visible JSX text in generated apps.
11
+ - `pnpm check` runs formatting, linting, effect-tsgo, i18n checks, private-skill availability checks, and the generated workspace contract.
12
+
13
+ ## Internationalization
14
+
15
+ Runtime i18n is enabled by default for generated apps. Agents must put user-visible UI copy in each app's `config/public/locales/<lang>/translation.json` and render it through `react-i18next` or `@modern-js/plugin-i18n/runtime`. Do not add hardcoded JSX text, `aria-label`, `title`, `alt`, or `placeholder` strings unless the value is a non-translatable technical token.
11
16
 
12
17
  ## Required Skill Baseline
13
18
 
@@ -1,7 +1,16 @@
1
- import { defineConfig } from "oxfmt";
2
- import ultracite from "ultracite/oxfmt";
1
+ import { defineConfig } from 'oxfmt';
2
+ import ultracite from 'ultracite/oxfmt';
3
3
 
4
4
  export default defineConfig({
5
5
  extends: [ultracite],
6
- ignorePatterns: ["dist", "node_modules", ".modern", ".modernjs", "**/routeTree.gen.ts"],
6
+ ignorePatterns: [
7
+ '.agents',
8
+ '**/*.json',
9
+ 'dist',
10
+ 'node_modules',
11
+ '.modern',
12
+ '.modernjs',
13
+ '**/routeTree.gen.ts',
14
+ ],
15
+ singleQuote: true,
7
16
  });
@@ -1,6 +1,6 @@
1
- import { defineConfig } from "oxlint";
2
- import core from "ultracite/oxlint/core";
3
- import react from "ultracite/oxlint/react";
1
+ import { defineConfig } from 'oxlint';
2
+ import core from 'ultracite/oxlint/core';
3
+ import react from 'ultracite/oxlint/react';
4
4
 
5
5
  export default defineConfig({
6
6
  env: {
@@ -8,5 +8,12 @@ export default defineConfig({
8
8
  node: true,
9
9
  },
10
10
  extends: [core, react],
11
- ignorePatterns: ["dist", "node_modules", ".modern", ".modernjs", "**/routeTree.gen.ts"],
11
+ ignorePatterns: [
12
+ '.agents',
13
+ 'dist',
14
+ 'node_modules',
15
+ '.modern',
16
+ '.modernjs',
17
+ '**/routeTree.gen.ts',
18
+ ],
12
19
  });
@@ -8,43 +8,37 @@ const lockPath = path.join(root, '.agents/skills-lock.json');
8
8
  const checkOnly = process.argv.includes('--check');
9
9
  const force = process.argv.includes('--force');
10
10
 
11
- function readJson(filePath) {
12
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
13
- }
11
+ const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf-8'));
14
12
 
15
- function run(command, args, options = {}) {
16
- return execFileSync(command, args, {
13
+ const run = (command, args, options = {}) =>
14
+ execFileSync(command, args, {
17
15
  cwd: options.cwd ?? root,
18
- encoding: 'utf8',
16
+ encoding: 'utf-8',
19
17
  stdio: options.stdio ?? ['ignore', 'pipe', 'pipe'],
20
18
  });
21
- }
22
19
 
23
- function cloneSource(source, targetDir) {
24
- const repo = source.repository.replace(/^https:\/\/github.com\//, '');
20
+ const cloneSource = (source, targetDir) => {
21
+ const repo = source.repository.replace(/^https:\/\/github.com\//u, '');
25
22
  try {
26
23
  run('gh', ['repo', 'clone', repo, targetDir, '--', '--depth', '1'], {
27
24
  stdio: 'inherit',
28
25
  });
29
- return;
30
26
  } catch {
31
27
  run('git', ['clone', '--depth', '1', source.repository, targetDir], {
32
28
  stdio: 'inherit',
33
29
  });
34
30
  }
35
- }
31
+ };
36
32
 
37
- function resolveSkillDir(sourceRoot, skillName) {
33
+ const resolveSkillDir = (sourceRoot, skillName) => {
38
34
  const candidates = [
39
35
  path.join(sourceRoot, skillName),
40
36
  path.join(sourceRoot, 'skills', skillName),
41
37
  path.join(sourceRoot, 'skills', 'engineering', skillName),
42
38
  path.join(sourceRoot, 'skills', 'productivity', skillName),
43
39
  ];
44
- return candidates.find(candidate =>
45
- fs.existsSync(path.join(candidate, 'SKILL.md')),
46
- );
47
- }
40
+ return candidates.find((candidate) => fs.existsSync(path.join(candidate, 'SKILL.md')));
41
+ };
48
42
 
49
43
  if (!fs.existsSync(lockPath)) {
50
44
  console.error('Missing .agents/skills-lock.json');
@@ -54,17 +48,14 @@ if (!fs.existsSync(lockPath)) {
54
48
  const lock = readJson(lockPath);
55
49
  const installDir = path.join(root, lock.installDir ?? '.agents/skills');
56
50
  const privateSources = (lock.sources ?? []).filter(
57
- source => source.install === 'clone-if-authorized',
51
+ (source) => source.install === 'clone-if-authorized',
58
52
  );
59
53
 
60
54
  if (checkOnly) {
61
- const missing = privateSources.flatMap(source =>
55
+ const missing = privateSources.flatMap((source) =>
62
56
  (source.baseline ?? [])
63
- .map(skill => skill.name)
64
- .filter(
65
- skillName =>
66
- !fs.existsSync(path.join(installDir, skillName, 'SKILL.md')),
67
- ),
57
+ .map((skill) => skill.name)
58
+ .filter((skillName) => !fs.existsSync(path.join(installDir, skillName, 'SKILL.md'))),
68
59
  );
69
60
  if (missing.length > 0) {
70
61
  console.warn(
@@ -85,9 +76,7 @@ for (const source of privateSources) {
85
76
  for (const skill of source.baseline ?? []) {
86
77
  const sourceSkillDir = resolveSkillDir(tempDir, skill.name);
87
78
  if (!sourceSkillDir) {
88
- throw new Error(
89
- `Skill ${skill.name} not found in ${source.repository}`,
90
- );
79
+ throw new Error(`Skill ${skill.name} not found in ${source.repository}`);
91
80
  }
92
81
  const targetSkillDir = path.join(installDir, skill.name);
93
82
  if (fs.existsSync(targetSkillDir)) {
@@ -95,12 +84,12 @@ for (const source of privateSources) {
95
84
  console.log(`Skipping existing ${skill.name}`);
96
85
  continue;
97
86
  }
98
- fs.rmSync(targetSkillDir, { recursive: true, force: true });
87
+ fs.rmSync(targetSkillDir, { force: true, recursive: true });
99
88
  }
100
89
  fs.cpSync(sourceSkillDir, targetSkillDir, { recursive: true });
101
90
  console.log(`Installed ${skill.name}`);
102
91
  }
103
92
  } finally {
104
- fs.rmSync(tempDir, { recursive: true, force: true });
93
+ fs.rmSync(tempDir, { force: true, recursive: true });
105
94
  }
106
95
  }