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

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.
Files changed (75) hide show
  1. package/README.md +146 -74
  2. package/bin/run.js +0 -0
  3. package/dist/cjs/create-package-root.cjs +65 -0
  4. package/dist/cjs/index.cjs +498 -0
  5. package/dist/cjs/locale/en.cjs +94 -0
  6. package/dist/cjs/locale/index.cjs +50 -0
  7. package/dist/cjs/locale/zh.cjs +94 -0
  8. package/dist/cjs/ultramodern-package-source.cjs +135 -0
  9. package/dist/cjs/ultramodern-workspace.cjs +6797 -0
  10. package/dist/esm/create-package-root.js +16 -0
  11. package/dist/esm/index.js +461 -0
  12. package/dist/esm/locale/en.js +56 -0
  13. package/dist/esm/locale/index.js +9 -0
  14. package/dist/esm/locale/zh.js +56 -0
  15. package/dist/esm/ultramodern-package-source.js +63 -0
  16. package/dist/esm/ultramodern-workspace.js +6738 -0
  17. package/dist/esm-node/create-package-root.js +17 -0
  18. package/dist/esm-node/index.js +462 -0
  19. package/dist/esm-node/locale/en.js +57 -0
  20. package/dist/esm-node/locale/index.js +10 -0
  21. package/dist/esm-node/locale/zh.js +57 -0
  22. package/dist/esm-node/ultramodern-package-source.js +64 -0
  23. package/dist/esm-node/ultramodern-workspace.js +6739 -0
  24. package/dist/types/create-package-root.d.ts +1 -0
  25. package/dist/types/locale/en.d.ts +7 -7
  26. package/dist/types/locale/index.d.ts +111 -2
  27. package/dist/types/locale/zh.d.ts +7 -7
  28. package/dist/types/ultramodern-package-source.d.ts +28 -0
  29. package/dist/types/ultramodern-workspace.d.ts +12 -3
  30. package/package.json +33 -15
  31. package/template-workspace/.agents/agent-reference-repos.json +24 -0
  32. package/template-workspace/.agents/skills-lock.json +19 -0
  33. package/template-workspace/.codex/hooks.json +16 -0
  34. package/template-workspace/.github/renovate.json +29 -0
  35. package/template-workspace/.github/workflows/ultramodern-workspace-gates.yml.handlebars +70 -0
  36. package/template-workspace/.gitignore.handlebars +5 -0
  37. package/template-workspace/.mise.toml.handlebars +2 -0
  38. package/template-workspace/AGENTS.md +43 -11
  39. package/template-workspace/README.md.handlebars +116 -11
  40. package/template-workspace/lefthook.yml +24 -0
  41. package/template-workspace/oxfmt.config.ts +1 -0
  42. package/template-workspace/oxlint.config.ts +1 -0
  43. package/template-workspace/pnpm-workspace.yaml +31 -8
  44. package/template-workspace/scripts/bootstrap-agent-skills.mjs +204 -21
  45. package/template-workspace/scripts/setup-agent-reference-repos.mjs +370 -0
  46. package/dist/index.js +0 -2626
  47. package/template/.agents/skills-lock.json +0 -34
  48. package/template/.browserslistrc +0 -4
  49. package/template/.github/workflows/ultramodern-gates.yml.handlebars +0 -30
  50. package/template/.gitignore.handlebars +0 -30
  51. package/template/.nvmrc +0 -2
  52. package/template/AGENTS.md +0 -25
  53. package/template/README.md +0 -79
  54. package/template/api/effect/index.ts.handlebars +0 -23
  55. package/template/api/lambda/hello.ts.handlebars +0 -6
  56. package/template/config/public/locales/cs/translation.json +0 -39
  57. package/template/config/public/locales/en/translation.json +0 -39
  58. package/template/modern.config.ts.handlebars +0 -53
  59. package/template/oxfmt.config.ts +0 -8
  60. package/template/oxlint.config.ts +0 -12
  61. package/template/package.json.handlebars +0 -67
  62. package/template/postcss.config.mjs.handlebars +0 -6
  63. package/template/scripts/bootstrap-agent-skills.mjs +0 -95
  64. package/template/scripts/check-i18n-strings.mjs +0 -83
  65. package/template/scripts/validate-ultramodern.mjs.handlebars +0 -178
  66. package/template/shared/effect/api.ts.handlebars +0 -17
  67. package/template/src/modern-app-env.d.ts +0 -1
  68. package/template/src/modern.runtime.ts.handlebars +0 -23
  69. package/template/src/routes/index.css.handlebars +0 -129
  70. package/template/src/routes/layout.tsx.handlebars +0 -9
  71. package/template/src/routes/page.tsx.handlebars +0 -155
  72. package/template/tailwind.config.ts.handlebars +0 -10
  73. package/template/tsconfig.json +0 -120
  74. package/template-workspace/scripts/check-i18n-strings.mjs +0 -83
  75. package/template-workspace/scripts/validate-ultramodern-workspace.mjs.handlebars +0 -433
@@ -3,23 +3,78 @@
3
3
  Generated UltraModern SuperApp workspace.
4
4
 
5
5
  This workspace keeps `presetUltramodern(...)` as the single public
6
- UltraModern.js 3.0 SuperApp surface and scaffolds the canonical Micro Vertical
7
- starter topology:
6
+ UltraModern.js 3.0 SuperApp surface and starts with an explicit shell:
8
7
 
9
- - `apps/shell-super-app` owns shell route assembly and remote manifest wiring.
10
- - `apps/remotes/remote-commerce` owns the commerce vertical remote.
11
- - `apps/remotes/remote-identity` owns the identity vertical remote.
12
- - `apps/remotes/remote-design-system` owns the horizontal design-system remote.
13
- - `services/service-recommendations-effect` owns the Effect BFF service.
14
- - `packages/shared-*` provide placeholders for cross-workspace contracts.
8
+ - `apps/shell-super-app` owns shell route assembly, Module Federation host
9
+ wiring, shared SSR/i18n runtime setup, and the boundary debugger.
10
+ - `packages/shared-*` provide placeholders for cross-workspace contracts,
11
+ design tokens, and Effect API sharing.
12
+ - `verticals/*` is intentionally empty until a real business domain is added.
15
13
 
16
- Run the scaffold validator before adding business code:
14
+ Add a full-stack MicroVertical when the product needs one:
17
15
 
18
16
  ```bash
19
- pnpm ultramodern:check
17
+ pnpm dlx @bleedingdev/modern-js-create transportation --vertical
18
+ pnpm dlx @bleedingdev/modern-js-create payments --vertical
20
19
  ```
21
20
 
22
- The topology and ownership metadata are generated under `topology/`.
21
+ Each added vertical owns its UI/routes, browser-safe Module Federation exposes,
22
+ private-first route metadata, localized URLs, public-route opt-ins, CSS prefix,
23
+ Effect BFF handlers, local API contract, and typed client surface. Server
24
+ handlers and Effect client/contract modules stay out of browser exposes.
25
+
26
+ ## Private-First Public Surfaces
27
+
28
+ Generated app routes are private and non-indexable by default. Author route
29
+ metadata in colocated `src/routes/**/route.meta.ts` files; the scaffold
30
+ regenerates `src/routes/ultramodern-route-metadata.ts` as a compatibility
31
+ manifest for config, i18n, public head, and public surface contracts. Private
32
+ app, auth, tenant, dashboard, and internal routes publish no discovery output
33
+ unless route metadata explicitly marks them `public && indexable`. The default
34
+ scaffold therefore emits only a disallowing `robots.txt`; sitemap, web
35
+ manifest, `llms.txt`, API catalog, security.txt, and JSON-LD output stay
36
+ omitted until a safe public route or public docs/help/product surface exists.
37
+
38
+ Public web artifacts are build/deploy outputs generated into `dist/public` and
39
+ `.output/public`, not hand-authored source files under `config/public`. Dynamic
40
+ public routes can expand sitemap entries through route-owned, Node-safe
41
+ `route.sitemap.mjs` providers beside route metadata. The public-surface
42
+ generator discovers those providers for dynamic public routes and still honors
43
+ explicit `routes.publicSurface.contentSources` entries in the generated
44
+ compatibility manifest.
45
+
46
+ Run the scaffold validator before adding business code and after every
47
+ `--vertical` mutation:
48
+
49
+ ```bash
50
+ mise install
51
+ pnpm install
52
+ pnpm check
53
+ pnpm build
54
+ ```
55
+
56
+ Generated CI does not call the local aggregate. It runs format, lint,
57
+ typecheck, skills, i18n boundary validation, contract validation, and build as
58
+ separate matrix jobs so failures are isolated and parallelizable.
59
+
60
+ By default, `pnpm install` also prepares read-only agent reference repositories
61
+ under `repos/` for Effect and UltraModern.js source lookup using squashed git
62
+ subtrees. Disable this setup with
63
+ `ULTRAMODERN_SKIP_AGENT_REPOS=1 pnpm install`, or rerun it
64
+ explicitly with `pnpm agents:refs:install`.
65
+
66
+ Agent skills are prepared during `pnpm install` as a developer convenience.
67
+ External skill repository failures do not block postinstall; strict installation
68
+ is available with `pnpm skills:install`. Use
69
+ `ULTRAMODERN_SKIP_AGENT_SKILLS=1` for a dependency install that avoids external
70
+ skill repositories completely.
71
+
72
+ The topology and ownership metadata are generated under `topology/`. The
73
+ workspace also ships `.github/workflows/ultramodern-workspace-gates.yml` and
74
+ `.github/renovate.json` with read-only workflow permissions, commit-pinned
75
+ actions, frozen installs, StepSecurity audit-mode runner hardening, dependency
76
+ dashboard review, one-day release age, grouped updates, and manual approval for
77
+ major upgrades.
23
78
 
24
79
  Package source metadata is generated at
25
80
  `.modernjs/ultramodern-package-source.json`. The default strategy keeps
@@ -27,3 +82,53 @@ UltraModern.js runtime and tooling packages on `workspace:*` for monorepo
27
82
  development. To create a workspace that can install those packages outside the
28
83
  monorepo, generate with `--ultramodern-package-source install`; generated shared
29
84
  packages still use `workspace:*` because they are part of this workspace.
85
+
86
+ ## Public URL Environment Variables
87
+
88
+ This workspace's apps no longer bake absolute `http://localhost:<port>` URLs
89
+ into asset configuration. Two environment variables now have distinct roles in
90
+ controlling where assets are served from and where SEO output links point.
91
+
92
+ | Variable | Role | Feeds |
93
+ | --- | --- | --- |
94
+ | `ULTRAMODERN_PUBLIC_URL_<APP_ID>` | Per-app deployment and asset origin | `output.assetPrefix`, Module Federation remote URLs |
95
+ | `MODERN_PUBLIC_SITE_URL` | Site-wide public origin for SEO output | Canonical, hreflang, sitemap `<loc>`, robots `Sitemap:` |
96
+
97
+ Asset URLs use this precedence: `ULTRAMODERN_PUBLIC_URL_<APP_ID>` →
98
+ `MODERN_PUBLIC_SITE_URL` → inferred workers.dev URL → origin-relative `/`.
99
+ SEO and site origin prefer: `MODERN_PUBLIC_SITE_URL` →
100
+ `ULTRAMODERN_PUBLIC_URL_<APP_ID>` → inferred workers.dev → `http://localhost:<port>`.
101
+
102
+ Without public URLs configured, asset paths are origin-relative (`/`), and the
103
+ dev server uses `dev.assetPrefix: '/'` — so apps work through tunnels and
104
+ reverse proxies (ngrok, cloudflared) without triggering Chrome's Local Network
105
+ Access prompt or mixed-content errors. Shell-only workspaces can set just
106
+ `MODERN_PUBLIC_SITE_URL` for SEO output.
107
+
108
+ ## Cloudflare Proof
109
+
110
+ Deploy the generated apps, then pass each deployed app's generated public URL
111
+ env key into the proof step. The proof script reads the generated contract and
112
+ checks the live Worker surface, including public-route sitemap/robots
113
+ consistency, preview noindex behavior, unknown-route status, asset headers,
114
+ byte budgets, and public sourcemap exposure. A shell-only workspace only needs
115
+ the shell URL; added verticals use the same `ULTRAMODERN_PUBLIC_URL_<APP_ID>`
116
+ pattern with hyphens converted to underscores and uppercased.
117
+
118
+ ```bash
119
+ pnpm cloudflare:deploy
120
+ ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP=https://shell-super-app.example.workers.dev \
121
+ pnpm cloudflare:proof -- --require-public-urls
122
+ ```
123
+
124
+ ## Troubleshooting
125
+
126
+ | Symptom | Current check | Owner |
127
+ | --- | --- | --- |
128
+ | Package cohort mismatch | Regenerate with one package source strategy, run `mise install`, then rerun `pnpm install` from the activated shell. | Generated workspace package source metadata |
129
+ | Install failure | Check the active Node/pnpm from `mise install`; rerun `pnpm install` after the shell sees the pinned versions. | Toolchain setup |
130
+ | Build failure | Run the matching primitive gate (`pnpm lint`, `pnpm typecheck`, `pnpm i18n:boundaries`, `pnpm contract:check`) before `pnpm build`; fix the owning failure first. | Owning package or generated contract |
131
+ | Missing public URL | Set the env key from `.modernjs/ultramodern-generated-contract.json`, for example `ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP`. | Deployment operator |
132
+ | Cloudflare credentials | Confirm Wrangler credentials before `pnpm cloudflare:deploy`; local checks do not prove live Worker access. | Deployment operator |
133
+ | Asset or CSS 404 | Rebuild with `pnpm build` or `pnpm cloudflare:deploy` and inspect emitted Modern/Rspack asset paths instead of hardcoding CSS URLs. | Framework/runtime asset pipeline |
134
+ | Federation manifest failure | Run the shell and vertical build scripts, then check each deployed `/mf-manifest.json` URL used by the shell. | Module Federation owner |
@@ -0,0 +1,24 @@
1
+ pre-commit:
2
+ commands:
3
+ format:
4
+ run: pnpm format
5
+ stage_fixed: true
6
+ lint-fix:
7
+ run: pnpm lint:fix
8
+ stage_fixed: true
9
+
10
+ pre-push:
11
+ parallel: true
12
+ commands:
13
+ format:
14
+ run: pnpm format:check
15
+ lint:
16
+ run: pnpm lint
17
+ typecheck:
18
+ run: pnpm typecheck
19
+ skills:
20
+ run: pnpm skills:check
21
+ i18n-boundaries:
22
+ run: pnpm i18n:boundaries
23
+ contract:
24
+ run: pnpm contract:check
@@ -8,6 +8,7 @@ export default defineConfig({
8
8
  '**/*.json',
9
9
  'dist',
10
10
  'node_modules',
11
+ 'repos/**',
11
12
  '.modern',
12
13
  '.modernjs',
13
14
  '**/routeTree.gen.ts',
@@ -12,6 +12,7 @@ export default defineConfig({
12
12
  '.agents',
13
13
  'dist',
14
14
  'node_modules',
15
+ 'repos/**',
15
16
  '.modern',
16
17
  '.modernjs',
17
18
  '**/routeTree.gen.ts',
@@ -1,17 +1,40 @@
1
1
  packages:
2
2
  - apps/*
3
- - apps/remotes/*
4
- - services/*
3
+ - verticals/*
5
4
  - packages/*
6
5
 
6
+ minimumReleaseAge: 1440
7
+ minimumReleaseAgeStrict: true
8
+ minimumReleaseAgeIgnoreMissingTime: false
9
+ minimumReleaseAgeExclude:
10
+ - '@bleedingdev/modern-js-*'
11
+ - '@tanstack/react-router'
12
+ - '@tanstack/router-core'
13
+ - '@typescript/native-preview'
14
+ - '@typescript/native-preview-*'
15
+ - '@types/react'
16
+ trustPolicy: no-downgrade
17
+ trustPolicyIgnoreAfter: 1440
18
+ blockExoticSubdeps: true
19
+ engineStrict: true
20
+ pmOnFail: error
21
+ verifyDepsBeforeRun: error
22
+ strictDepBuilds: true
23
+
24
+ peerDependencyRules:
25
+ allowedVersions:
26
+ react: '>=19.0.0'
27
+ typescript: '>=6.0.0'
28
+
29
+ overrides:
30
+ '@tanstack/react-router': 1.170.15
31
+ node-fetch: '^3.3.2'
32
+
7
33
  allowBuilds:
8
34
  '@swc/core': true
9
35
  core-js: true
10
36
  esbuild: true
37
+ lefthook: true
11
38
  msgpackr-extract: true
12
-
13
- onlyBuiltDependencies:
14
- - '@swc/core'
15
- - core-js
16
- - esbuild
17
- - msgpackr-extract
39
+ sharp: true
40
+ workerd: true
@@ -7,26 +7,153 @@ const root = process.cwd();
7
7
  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
+ const postinstall = process.argv.includes('--postinstall');
11
+ const truthy = value => /^(1|true|yes|on)$/i.test(String(value ?? ''));
12
+ const falsy = value => /^(0|false|no|off)$/i.test(String(value ?? ''));
13
+ const skipRequested =
14
+ truthy(process.env.ULTRAMODERN_SKIP_AGENT_SKILLS) ||
15
+ falsy(process.env.ULTRAMODERN_AGENT_SKILLS);
16
+ const cloneTimeoutMs = Number.parseInt(
17
+ process.env.ULTRAMODERN_AGENT_SKILLS_CLONE_TIMEOUT_MS ?? '60000',
18
+ 10,
19
+ );
10
20
 
11
- const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf-8'));
21
+ const readJson = filePath => JSON.parse(fs.readFileSync(filePath, 'utf-8'));
12
22
 
13
23
  const run = (command, args, options = {}) =>
14
24
  execFileSync(command, args, {
15
25
  cwd: options.cwd ?? root,
16
26
  encoding: 'utf-8',
17
27
  stdio: options.stdio ?? ['ignore', 'pipe', 'pipe'],
28
+ timeout: options.timeout,
29
+ });
30
+
31
+ const commandExists = command => {
32
+ try {
33
+ run(command, ['--version'], { stdio: 'ignore' });
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ };
39
+
40
+ const runShell = script =>
41
+ run('sh', ['-lc', script], {
42
+ stdio: 'inherit',
43
+ });
44
+
45
+ const installGit = () => {
46
+ if (commandExists('git')) {
47
+ return;
48
+ }
49
+
50
+ if (commandExists('brew')) {
51
+ run('brew', ['install', 'git'], { stdio: 'inherit' });
52
+ } else if (process.platform === 'linux' && commandExists('apt-get')) {
53
+ const sudo =
54
+ typeof process.getuid === 'function' && process.getuid() === 0
55
+ ? ''
56
+ : 'sudo ';
57
+ runShell(`${sudo}apt-get update && ${sudo}apt-get install -y git`);
58
+ } else if (process.platform === 'linux' && commandExists('dnf')) {
59
+ const sudo =
60
+ typeof process.getuid === 'function' && process.getuid() === 0
61
+ ? ''
62
+ : 'sudo ';
63
+ runShell(`${sudo}dnf install -y git`);
64
+ } else if (process.platform === 'linux' && commandExists('yum')) {
65
+ const sudo =
66
+ typeof process.getuid === 'function' && process.getuid() === 0
67
+ ? ''
68
+ : 'sudo ';
69
+ runShell(`${sudo}yum install -y git`);
70
+ } else if (process.platform === 'linux' && commandExists('apk')) {
71
+ runShell('apk add --no-cache git');
72
+ }
73
+
74
+ if (!commandExists('git')) {
75
+ throw new Error(
76
+ 'Git is required for UltraModern setup. Install git and run pnpm skills:install again.',
77
+ );
78
+ }
79
+ };
80
+
81
+ const isInsideGitWorkTree = () => {
82
+ try {
83
+ return run('git', ['rev-parse', '--is-inside-work-tree']).trim() === 'true';
84
+ } catch {
85
+ return false;
86
+ }
87
+ };
88
+
89
+ const initializeGitRepository = () => {
90
+ if (isInsideGitWorkTree()) {
91
+ return;
92
+ }
93
+
94
+ try {
95
+ run('git', ['init', '-b', 'main'], { stdio: 'inherit' });
96
+ } catch {
97
+ run('git', ['init'], { stdio: 'inherit' });
98
+ run('git', ['branch', '-M', 'main'], { stdio: 'inherit' });
99
+ }
100
+ };
101
+
102
+ const installLefthook = () => {
103
+ try {
104
+ run('lefthook', ['install'], { stdio: 'inherit' });
105
+ } catch (error) {
106
+ console.warn(`Unable to install lefthook hooks: ${error.message}`);
107
+ }
108
+ };
109
+
110
+ const removeTree = dir =>
111
+ fs.rmSync(dir, {
112
+ force: true,
113
+ maxRetries: 5,
114
+ recursive: true,
115
+ retryDelay: 100,
18
116
  });
19
117
 
20
118
  const cloneSource = (source, targetDir) => {
119
+ if (source.commit) {
120
+ run('git', ['init', targetDir], { timeout: 30000 });
121
+ run('git', ['remote', 'add', 'origin', source.repository], {
122
+ cwd: targetDir,
123
+ timeout: 30000,
124
+ });
125
+ run('git', ['fetch', '--depth', '1', '--quiet', 'origin', source.commit], {
126
+ cwd: targetDir,
127
+ timeout: cloneTimeoutMs,
128
+ });
129
+ run(
130
+ 'git',
131
+ [
132
+ '-c',
133
+ 'advice.detachedHead=false',
134
+ 'checkout',
135
+ '--detach',
136
+ '--quiet',
137
+ 'FETCH_HEAD',
138
+ ],
139
+ { cwd: targetDir, timeout: 30000 },
140
+ );
141
+ return;
142
+ }
143
+
21
144
  const repo = source.repository.replace(/^https:\/\/github.com\//u, '');
22
145
  try {
23
- run('gh', ['repo', 'clone', repo, targetDir, '--', '--depth', '1'], {
24
- stdio: 'inherit',
25
- });
146
+ run(
147
+ 'gh',
148
+ ['repo', 'clone', repo, targetDir, '--', '--depth', '1', '--quiet'],
149
+ { timeout: cloneTimeoutMs },
150
+ );
26
151
  } catch {
27
- run('git', ['clone', '--depth', '1', source.repository, targetDir], {
28
- stdio: 'inherit',
29
- });
152
+ run(
153
+ 'git',
154
+ ['clone', '--depth', '1', '--quiet', source.repository, targetDir],
155
+ { timeout: cloneTimeoutMs },
156
+ );
30
157
  }
31
158
  };
32
159
 
@@ -37,7 +164,9 @@ const resolveSkillDir = (sourceRoot, skillName) => {
37
164
  path.join(sourceRoot, 'skills', 'engineering', skillName),
38
165
  path.join(sourceRoot, 'skills', 'productivity', skillName),
39
166
  ];
40
- return candidates.find((candidate) => fs.existsSync(path.join(candidate, 'SKILL.md')));
167
+ return candidates.find(candidate =>
168
+ fs.existsSync(path.join(candidate, 'SKILL.md')),
169
+ );
41
170
  };
42
171
 
43
172
  if (!fs.existsSync(lockPath)) {
@@ -47,36 +176,88 @@ if (!fs.existsSync(lockPath)) {
47
176
 
48
177
  const lock = readJson(lockPath);
49
178
  const installDir = path.join(root, lock.installDir ?? '.agents/skills');
50
- const privateSources = (lock.sources ?? []).filter(
51
- (source) => source.install === 'clone-if-authorized',
179
+ const sources = lock.sources ?? [];
180
+ const requiredCloneSources = sources.filter(
181
+ source => source.install === 'clone',
182
+ );
183
+ const optionalCloneSources = sources.filter(
184
+ source => source.install === 'clone-if-authorized',
185
+ );
186
+ const requiredSkills = [
187
+ ...(lock.baseline ?? []),
188
+ ...requiredCloneSources.flatMap(source => source.baseline ?? []),
189
+ ].filter(
190
+ (skill, index, skills) =>
191
+ skills.findIndex(candidate => candidate.name === skill.name) === index,
52
192
  );
53
193
 
194
+ if (skipRequested) {
195
+ const reason = 'agent skills bootstrap skipped by environment';
196
+ if (checkOnly) {
197
+ console.log(reason);
198
+ process.exit(0);
199
+ }
200
+ console.log(reason);
201
+ installLefthook();
202
+ process.exit(0);
203
+ }
204
+
54
205
  if (checkOnly) {
55
- const missing = privateSources.flatMap((source) =>
206
+ const missingRequired = requiredSkills
207
+ .map(skill => skill.name)
208
+ .filter(
209
+ skillName => !fs.existsSync(path.join(installDir, skillName, 'SKILL.md')),
210
+ );
211
+ const missingOptional = optionalCloneSources.flatMap(source =>
56
212
  (source.baseline ?? [])
57
- .map((skill) => skill.name)
58
- .filter((skillName) => !fs.existsSync(path.join(installDir, skillName, 'SKILL.md'))),
213
+ .map(skill => skill.name)
214
+ .filter(
215
+ skillName =>
216
+ !fs.existsSync(path.join(installDir, skillName, 'SKILL.md')),
217
+ ),
59
218
  );
60
- if (missing.length > 0) {
219
+
220
+ if (missingRequired.length > 0) {
221
+ console.error(
222
+ `Required agent skills not installed: ${missingRequired.join(', ')}. Run pnpm skills:install.`,
223
+ );
224
+ process.exit(1);
225
+ }
226
+
227
+ if (missingOptional.length > 0) {
61
228
  console.warn(
62
- `Private skills not installed: ${missing.join(', ')}. Run pnpm skills:install if you have access.`,
229
+ `Private skills not installed: ${missingOptional.join(', ')}. Run pnpm skills:install if you have access.`,
63
230
  );
64
231
  } else {
65
- console.log('Agent skills are installed.');
232
+ console.log('Required and private agent skills are installed.');
233
+ process.exit(0);
66
234
  }
235
+ console.log('Required agent skills are installed.');
67
236
  process.exit(0);
68
237
  }
69
238
 
70
239
  fs.mkdirSync(installDir, { recursive: true });
240
+ installGit();
241
+ initializeGitRepository();
71
242
 
72
- for (const source of privateSources) {
243
+ for (const source of [...requiredCloneSources, ...optionalCloneSources]) {
73
244
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ultramodern-skills-'));
74
245
  try {
75
- cloneSource(source, tempDir);
246
+ try {
247
+ cloneSource(source, tempDir);
248
+ } catch (error) {
249
+ if (source.install === 'clone-if-authorized' || postinstall) {
250
+ console.warn(`Skipping ${source.repository}; ${error.message}`);
251
+ continue;
252
+ }
253
+ throw error;
254
+ }
76
255
  for (const skill of source.baseline ?? []) {
77
256
  const sourceSkillDir = resolveSkillDir(tempDir, skill.name);
78
257
  if (!sourceSkillDir) {
79
- throw new Error(`Skill ${skill.name} not found in ${source.repository}`);
258
+ throw new Error(
259
+ `Skill ${skill.name} not found in ${source.repository}`,
260
+ );
80
261
  }
81
262
  const targetSkillDir = path.join(installDir, skill.name);
82
263
  if (fs.existsSync(targetSkillDir)) {
@@ -84,12 +265,14 @@ for (const source of privateSources) {
84
265
  console.log(`Skipping existing ${skill.name}`);
85
266
  continue;
86
267
  }
87
- fs.rmSync(targetSkillDir, { force: true, recursive: true });
268
+ removeTree(targetSkillDir);
88
269
  }
89
270
  fs.cpSync(sourceSkillDir, targetSkillDir, { recursive: true });
90
271
  console.log(`Installed ${skill.name}`);
91
272
  }
92
273
  } finally {
93
- fs.rmSync(tempDir, { force: true, recursive: true });
274
+ removeTree(tempDir);
94
275
  }
95
276
  }
277
+
278
+ installLefthook();