mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(devex): generate frontend provider-chain docs + CI drift gate (#3975)
This commit is contained in:
@@ -32,6 +32,7 @@ jobs:
|
||||
outputs:
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
i18n: ${{ steps.filter.outputs.i18n }}
|
||||
docs: ${{ steps.filter.outputs.docs }}
|
||||
rust-core: ${{ steps.filter.outputs['rust-core'] }}
|
||||
rust-tauri: ${{ steps.filter.outputs['rust-tauri'] }}
|
||||
playwright: ${{ steps.filter.outputs.playwright }}
|
||||
@@ -61,6 +62,13 @@ jobs:
|
||||
i18n:
|
||||
- '.github/workflows/pr-ci.yml'
|
||||
- 'app/src/**'
|
||||
docs:
|
||||
- '.github/workflows/pr-ci.yml'
|
||||
- 'app/src/App.tsx'
|
||||
- 'scripts/generate-architecture-docs.mjs'
|
||||
- 'scripts/__tests__/generate-architecture-docs.test.mjs'
|
||||
- 'gitbooks/developing/architecture/frontend.md'
|
||||
- 'package.json'
|
||||
rust-core:
|
||||
- '.github/workflows/pr-ci.yml'
|
||||
- 'Cargo.toml'
|
||||
@@ -177,6 +185,31 @@ jobs:
|
||||
- name: Verify i18n coverage (missing / extra / drifted keys across locales)
|
||||
run: pnpm i18n:check
|
||||
|
||||
docs-drift:
|
||||
name: Docs Drift (generated architecture docs)
|
||||
needs: [changes]
|
||||
if: needs.changes.outputs.docs == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
container:
|
||||
image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0
|
||||
steps:
|
||||
- name: Checkout code
|
||||
# Pinned to the v5 commit SHA for supply-chain hardening (CodeRabbit
|
||||
# review on #3892). The other lanes still use the @v5 tag; migrating
|
||||
# them is a separate repo-wide change.
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
# The generator + its tests use only Node built-ins (no workspace deps),
|
||||
# so there is no `pnpm install` step — this lane stays fast.
|
||||
- name: Generator unit tests
|
||||
run: pnpm docs:test
|
||||
|
||||
- name: Verify generated architecture docs are not stale
|
||||
run: pnpm docs:check
|
||||
|
||||
rust-quality:
|
||||
name: Rust Quality (fmt, clippy)
|
||||
needs: [changes]
|
||||
@@ -808,6 +841,7 @@ jobs:
|
||||
- changes
|
||||
- frontend-quality
|
||||
- i18n-coverage
|
||||
- docs-drift
|
||||
- rust-quality
|
||||
- build-playwright-e2e-artifact
|
||||
- frontend-coverage
|
||||
@@ -827,6 +861,7 @@ jobs:
|
||||
["Detect Changed Areas"]="${{ needs.changes.result }}"
|
||||
["Frontend Quality"]="${{ needs['frontend-quality'].result }}"
|
||||
["i18n Coverage"]="${{ needs['i18n-coverage'].result }}"
|
||||
["Docs Drift"]="${{ needs['docs-drift'].result }}"
|
||||
["Rust Quality"]="${{ needs['rust-quality'].result }}"
|
||||
["Build Playwright E2E Artifact"]="${{ needs['build-playwright-e2e-artifact'].result }}"
|
||||
["Frontend Coverage"]="${{ needs['frontend-coverage'].result }}"
|
||||
|
||||
@@ -233,6 +233,7 @@ Each domain owns `bus.rs` with handlers. Convention: `<Purpose>Subscriber`, `nam
|
||||
- **i18n**: all UI text through `useT()` from `app/src/lib/i18n/I18nContext`. Add key to `en.ts` **and real translations to all locale files** (`ar`, `bn`, `de`, `es`, `fr`, `hi`, `id`, `it`, `ko`, `pl`, `pt`, `ru`, `zh-CN`). CI enforces parity (`pnpm i18n:check`) and detects English placeholders (`pnpm i18n:english:check`).
|
||||
- **Dual socket sync**: keep `socketService`/MCP transport aligned with core socket behavior.
|
||||
- **Tauri guard**: use `isTauri()` or wrap `invoke(...)` in try/catch — never check `window.__TAURI__` directly.
|
||||
- **Generated docs**: some architecture docs contain generated blocks marked `<!-- BEGIN/END GENERATED: … -->` sourced from code (today: the frontend provider chain in [`gitbooks/developing/architecture/frontend.md`](gitbooks/developing/architecture/frontend.md), from the `@generated-source:provider-chain` marker in `app/src/App.tsx`). Don't hand-edit between the markers — update the code source, then run `pnpm docs:generate`. CI (`pnpm docs:check`, the **Docs Drift** lane) fails on stale generated docs. Generator + tests: `scripts/generate-architecture-docs.mjs`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -103,6 +103,28 @@ function App() {
|
||||
const socketWrapped = (children: React.ReactNode) =>
|
||||
onMobile ? <>{children}</> : <SocketProvider>{children}</SocketProvider>;
|
||||
|
||||
/*
|
||||
* @generated-source:provider-chain
|
||||
* Authoritative top-level provider / gate nesting for the desktop shell,
|
||||
* outermost first. Keep this list in sync with the JSX returned below;
|
||||
* `scripts/generate-architecture-docs.mjs` renders it into
|
||||
* `gitbooks/developing/architecture/frontend.md` and CI (`pnpm docs:check`)
|
||||
* fails if the doc drifts. Refresh the doc with `pnpm docs:generate`.
|
||||
* Format per line: `<order>. <Component> — <role>` (role must not contain " — ").
|
||||
* 1. Sentry.ErrorBoundary — Crash boundary; renders ErrorFallbackScreen
|
||||
* 2. Provider — Redux store; enables useAppSelector / dispatch app-wide
|
||||
* 3. PersistGate — Holds UI until persisted Redux slices rehydrate
|
||||
* 4. ThemeProvider — Theme tokens and dark-mode handling
|
||||
* 5. I18nProvider — Localization context consumed via useT
|
||||
* 6. BootCheckGate — Blocks render until the core boot snapshot resolves
|
||||
* 7. CoreStateProvider — Core app snapshot: auth, session, onboarding state
|
||||
* 8. SocketProvider — Core socket.io events; desktop only (mobile uses the TunnelTransport relay)
|
||||
* 9. ChatRuntimeProvider — Chat runtime events, tool timeline, and approvals
|
||||
* 10. Router — HashRouter navigation for all routes
|
||||
* 11. CommandProvider — Command palette context
|
||||
* 12. ServiceBlockingGate — Blocks the shell until required services are configured
|
||||
* @end-source:provider-chain
|
||||
*/
|
||||
return (
|
||||
<Sentry.ErrorBoundary
|
||||
fallback={({ error, componentStack, resetError }) => (
|
||||
|
||||
@@ -70,24 +70,34 @@ OpenHuman’s desktop UI is a **React 19** app (`app/src/`) that:
|
||||
|
||||
### Provider chain
|
||||
|
||||
```
|
||||
Redux Provider
|
||||
└─ PersistGate
|
||||
└─ UserProvider
|
||||
└─ SocketProvider
|
||||
└─ AIProvider
|
||||
└─ SkillProvider
|
||||
└─ HashRouter
|
||||
└─ AppRoutes (pages + settings)
|
||||
```
|
||||
<!-- BEGIN GENERATED: provider-chain — source app/src/App.tsx via scripts/generate-architecture-docs.mjs; do not edit between markers (run `pnpm docs:generate`) -->
|
||||
|
||||
_Generated from `app/src/App.tsx` by `scripts/generate-architecture-docs.mjs`. Do not edit by hand — run `pnpm docs:generate` to refresh._
|
||||
|
||||
| # | Component | Role |
|
||||
| --- | --- | --- |
|
||||
| 1 | `Sentry.ErrorBoundary` | Crash boundary; renders ErrorFallbackScreen |
|
||||
| 2 | `Provider` | Redux store; enables useAppSelector / dispatch app-wide |
|
||||
| 3 | `PersistGate` | Holds UI until persisted Redux slices rehydrate |
|
||||
| 4 | `ThemeProvider` | Theme tokens and dark-mode handling |
|
||||
| 5 | `I18nProvider` | Localization context consumed via useT |
|
||||
| 6 | `BootCheckGate` | Blocks render until the core boot snapshot resolves |
|
||||
| 7 | `CoreStateProvider` | Core app snapshot: auth, session, onboarding state |
|
||||
| 8 | `SocketProvider` | Core socket.io events; desktop only (mobile uses the TunnelTransport relay) |
|
||||
| 9 | `ChatRuntimeProvider` | Chat runtime events, tool timeline, and approvals |
|
||||
| 10 | `Router` | HashRouter navigation for all routes |
|
||||
| 11 | `CommandProvider` | Command palette context |
|
||||
| 12 | `ServiceBlockingGate` | Blocks the shell until required services are configured |
|
||||
|
||||
<!-- END GENERATED: provider-chain -->
|
||||
|
||||
**Why this order**
|
||||
|
||||
1. Redux is outermost for `useAppSelector` / dispatch everywhere.
|
||||
2. `PersistGate` rehydrates persisted slices before children assume stable auth.
|
||||
3. `SocketProvider` uses the auth token for Socket.io.
|
||||
4. `AIProvider` / `SkillProvider` wrap features that depend on socket and store state.
|
||||
5. `HashRouter` supplies navigation to all routes.
|
||||
1. Redux `Provider` is outermost so `useAppSelector` / dispatch work everywhere.
|
||||
2. `PersistGate` rehydrates persisted slices before children assume stable auth/session.
|
||||
3. `BootCheckGate` / `CoreStateProvider` resolve the core boot snapshot (auth, onboarding) before feature providers mount.
|
||||
4. `SocketProvider` (desktop only) and `ChatRuntimeProvider` depend on that core state for realtime events and approvals.
|
||||
5. `Router` supplies navigation to all routes.
|
||||
|
||||
### Module relationships (simplified)
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm --filter openhuman-app build",
|
||||
"docs:generate": "node scripts/generate-architecture-docs.mjs",
|
||||
"docs:check": "node scripts/generate-architecture-docs.mjs --check",
|
||||
"docs:test": "node --test scripts/__tests__/generate-architecture-docs.test.mjs",
|
||||
"compile": "pnpm --filter openhuman-app compile",
|
||||
"dev": "pnpm --filter openhuman-app dev",
|
||||
"dev:app": "pnpm --filter openhuman-app dev:app",
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// Tests for the architecture-docs generator (issue #3892).
|
||||
// Run: node --test scripts/__tests__/generate-architecture-docs.test.mjs
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import {
|
||||
APP_TSX,
|
||||
FRONTEND_DOC,
|
||||
computeFrontendDoc,
|
||||
parseProviderChain,
|
||||
renderProviderChainBody,
|
||||
spliceGeneratedBlock,
|
||||
} from '../generate-architecture-docs.mjs';
|
||||
|
||||
const SAMPLE = `
|
||||
/*
|
||||
* @generated-source:provider-chain
|
||||
* 1. Sentry.ErrorBoundary — Crash boundary; renders ErrorFallbackScreen
|
||||
* 2. Provider — Redux store; enables useAppSelector / dispatch app-wide
|
||||
* @end-source:provider-chain
|
||||
*/
|
||||
`;
|
||||
|
||||
test('parseProviderChain extracts ordered rows from the marker block', () => {
|
||||
const providers = parseProviderChain(SAMPLE);
|
||||
assert.equal(providers.length, 2);
|
||||
assert.deepEqual(providers[0], {
|
||||
order: 1,
|
||||
name: 'Sentry.ErrorBoundary',
|
||||
role: 'Crash boundary; renders ErrorFallbackScreen',
|
||||
});
|
||||
assert.equal(providers[1].name, 'Provider');
|
||||
});
|
||||
|
||||
test('parseProviderChain throws when the marker block is missing', () => {
|
||||
assert.throws(() => parseProviderChain('// no markers here'), /source marker not found/);
|
||||
});
|
||||
|
||||
test('parseProviderChain throws when ordering is non-contiguous (drifted metadata)', () => {
|
||||
const bad = `@generated-source:provider-chain
|
||||
* 1. A — first
|
||||
* 3. C — third
|
||||
@end-source:provider-chain`;
|
||||
assert.throws(() => parseProviderChain(bad), /numbered 1\.\.N/);
|
||||
});
|
||||
|
||||
test('parseProviderChain rejects a role containing a table-breaking pipe', () => {
|
||||
const bad = `@generated-source:provider-chain
|
||||
* 1. A — has a | pipe
|
||||
@end-source:provider-chain`;
|
||||
assert.throws(() => parseProviderChain(bad), /must not contain a "\|"/);
|
||||
});
|
||||
|
||||
test('renderProviderChainBody is deterministic and table-shaped', () => {
|
||||
const body = renderProviderChainBody(parseProviderChain(SAMPLE));
|
||||
assert.equal(body[0], '');
|
||||
assert.equal(body[3], '| # | Component | Role |');
|
||||
assert.equal(body[4], '| --- | --- | --- |');
|
||||
assert.equal(body[5], '| 1 | `Sentry.ErrorBoundary` | Crash boundary; renders ErrorFallbackScreen |');
|
||||
assert.equal(body[body.length - 1], '');
|
||||
});
|
||||
|
||||
test('spliceGeneratedBlock replaces only the content between markers', () => {
|
||||
const doc = [
|
||||
'# Title',
|
||||
'<!-- BEGIN GENERATED: provider-chain -->',
|
||||
'stale row',
|
||||
'<!-- END GENERATED: provider-chain -->',
|
||||
'after',
|
||||
].join('\n');
|
||||
const out = spliceGeneratedBlock(doc, ['', 'fresh', '']);
|
||||
assert.equal(
|
||||
out,
|
||||
[
|
||||
'# Title',
|
||||
'<!-- BEGIN GENERATED: provider-chain -->',
|
||||
'',
|
||||
'fresh',
|
||||
'',
|
||||
'<!-- END GENERATED: provider-chain -->',
|
||||
'after',
|
||||
].join('\n')
|
||||
);
|
||||
});
|
||||
|
||||
test('spliceGeneratedBlock throws when markers are absent', () => {
|
||||
assert.throws(() => spliceGeneratedBlock('no markers', ['x']), /markers not found/);
|
||||
});
|
||||
|
||||
test('committed frontend.md is in sync with App.tsx (the CI drift gate)', () => {
|
||||
const { updated, current } = computeFrontendDoc();
|
||||
assert.equal(updated, current, 'run `pnpm docs:generate` and commit the result');
|
||||
});
|
||||
|
||||
test('App.tsx still reflects the post-CoreState/ChatRuntime provider chain', () => {
|
||||
// Guards against regressing the doc back to the pre-audit provider names.
|
||||
const names = parseProviderChain(readFileSync(APP_TSX, 'utf8')).map(p => p.name);
|
||||
assert.ok(names.includes('CoreStateProvider'));
|
||||
assert.ok(names.includes('ChatRuntimeProvider'));
|
||||
assert.ok(!names.includes('UserProvider'), 'UserProvider was removed pre-audit');
|
||||
assert.ok(!names.includes('AIProvider'), 'AIProvider was removed pre-audit');
|
||||
assert.ok(!names.includes('SkillProvider'), 'SkillProvider was removed pre-audit');
|
||||
// Sanity: the doc path the gate guards actually exists.
|
||||
assert.ok(FRONTEND_DOC.endsWith('gitbooks/developing/architecture/frontend.md'));
|
||||
});
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* generate-architecture-docs.mjs
|
||||
* --------------------------------
|
||||
*
|
||||
* Minimal "docs generated from code" slice for issue #3892. Derives the
|
||||
* frontend **provider chain** from an explicit source marker in
|
||||
* `app/src/App.tsx` and renders it into a marked block in
|
||||
* `gitbooks/developing/architecture/frontend.md`, so the documented chain
|
||||
* cannot silently drift from the code (the exact drift called out in the
|
||||
* issue: the doc described `UserProvider` / `AIProvider` / `SkillProvider`
|
||||
* long after the code moved to `CoreStateProvider` / `ChatRuntimeProvider`).
|
||||
*
|
||||
* Modes:
|
||||
* node scripts/generate-architecture-docs.mjs # write (refresh docs)
|
||||
* node scripts/generate-architecture-docs.mjs --check # CI: fail on drift
|
||||
*
|
||||
* Source of truth: the `@generated-source:provider-chain` block in App.tsx.
|
||||
* This is an intentionally tiny, hand-auditable slice. Generating controller /
|
||||
* tool / skill reference tables from the Rust registry is the follow-up (see
|
||||
* the PR body); the splice + check harness here is built to extend to more
|
||||
* blocks without changing the CI wiring.
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = resolve(HERE, '..');
|
||||
|
||||
export const APP_TSX = resolve(REPO_ROOT, 'app/src/App.tsx');
|
||||
export const FRONTEND_DOC = resolve(REPO_ROOT, 'gitbooks/developing/architecture/frontend.md');
|
||||
|
||||
/** Stable tokens used to locate the generated block in the doc. */
|
||||
const BLOCK_BEGIN_TOKEN = 'BEGIN GENERATED: provider-chain';
|
||||
const BLOCK_END_TOKEN = 'END GENERATED: provider-chain';
|
||||
|
||||
/**
|
||||
* Parse the ordered provider chain out of the `@generated-source:provider-chain`
|
||||
* marker block in App.tsx source text.
|
||||
*
|
||||
* @param {string} appSource - contents of App.tsx
|
||||
* @returns {{ order: number, name: string, role: string }[]}
|
||||
* @throws if the marker block is missing, empty, or rows are malformed / non-contiguous
|
||||
*/
|
||||
export function parseProviderChain(appSource) {
|
||||
const begin = appSource.indexOf('@generated-source:provider-chain');
|
||||
const end = appSource.indexOf('@end-source:provider-chain');
|
||||
if (begin === -1 || end === -1 || end < begin) {
|
||||
throw new Error(
|
||||
'provider-chain source marker not found in App.tsx; expected ' +
|
||||
'`@generated-source:provider-chain` … `@end-source:provider-chain`'
|
||||
);
|
||||
}
|
||||
const region = appSource.slice(begin, end);
|
||||
const rowRe = /^\s*\*?\s*(\d+)\.\s+(.+?)\s+—\s+(.+?)\s*$/;
|
||||
const providers = [];
|
||||
for (const line of region.split('\n')) {
|
||||
const m = rowRe.exec(line);
|
||||
if (!m) continue;
|
||||
providers.push({ order: Number(m[1]), name: m[2].trim(), role: m[3].trim() });
|
||||
}
|
||||
if (providers.length === 0) {
|
||||
throw new Error('provider-chain source marker contained no `N. Component — role` rows');
|
||||
}
|
||||
// Guard against drift in the marker itself: orders must be 1..N, in order,
|
||||
// names non-empty, and roles free of the `|` that would break the table.
|
||||
providers.forEach((p, i) => {
|
||||
if (p.order !== i + 1) {
|
||||
throw new Error(
|
||||
`provider-chain rows must be numbered 1..N in order; row ${i + 1} is numbered ${p.order}`
|
||||
);
|
||||
}
|
||||
if (!p.name) throw new Error(`provider-chain row ${p.order} is missing a component name`);
|
||||
if (!p.role) throw new Error(`provider-chain row ${p.order} is missing a role`);
|
||||
if (p.name.includes('|') || p.role.includes('|')) {
|
||||
throw new Error(`provider-chain row ${p.order} must not contain a "|" character`);
|
||||
}
|
||||
});
|
||||
return providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the markdown body that lives between the BEGIN/END markers.
|
||||
* Returned as an array of lines (no trailing join) so splicing stays
|
||||
* deterministic and line-oriented.
|
||||
*
|
||||
* @param {{ order: number, name: string, role: string }[]} providers
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function renderProviderChainBody(providers) {
|
||||
return [
|
||||
'',
|
||||
'_Generated from `app/src/App.tsx` by `scripts/generate-architecture-docs.mjs`. ' +
|
||||
'Do not edit by hand — run `pnpm docs:generate` to refresh._',
|
||||
'',
|
||||
'| # | Component | Role |',
|
||||
'| --- | --- | --- |',
|
||||
...providers.map(p => `| ${p.order} | \`${p.name}\` | ${p.role} |`),
|
||||
'',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Splice freshly rendered body lines into `docSource` between the BEGIN/END
|
||||
* marker lines (markers preserved). Returns the updated document text.
|
||||
*
|
||||
* @param {string} docSource
|
||||
* @param {string[]} bodyLines
|
||||
* @returns {string}
|
||||
* @throws if the markers are missing or out of order
|
||||
*/
|
||||
export function spliceGeneratedBlock(docSource, bodyLines) {
|
||||
const lines = docSource.split('\n');
|
||||
const beginIdx = lines.findIndex(l => l.includes(BLOCK_BEGIN_TOKEN));
|
||||
const endIdx = lines.findIndex(l => l.includes(BLOCK_END_TOKEN));
|
||||
if (beginIdx === -1 || endIdx === -1) {
|
||||
throw new Error(
|
||||
`generated-block markers not found in ${FRONTEND_DOC}; expected HTML comments ` +
|
||||
`containing "${BLOCK_BEGIN_TOKEN}" and "${BLOCK_END_TOKEN}"`
|
||||
);
|
||||
}
|
||||
if (endIdx <= beginIdx) {
|
||||
throw new Error('generated-block END marker must come after the BEGIN marker');
|
||||
}
|
||||
const next = [...lines.slice(0, beginIdx + 1), ...bodyLines, ...lines.slice(endIdx)];
|
||||
return next.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the desired doc content from the current sources.
|
||||
* @returns {{ updated: string, current: string }}
|
||||
*/
|
||||
export function computeFrontendDoc() {
|
||||
const providers = parseProviderChain(readFileSync(APP_TSX, 'utf8'));
|
||||
const current = readFileSync(FRONTEND_DOC, 'utf8');
|
||||
const updated = spliceGeneratedBlock(current, renderProviderChainBody(providers));
|
||||
return { updated, current };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const check = process.argv.includes('--check');
|
||||
const { updated, current } = computeFrontendDoc();
|
||||
|
||||
if (check) {
|
||||
if (updated !== current) {
|
||||
console.error(
|
||||
'✗ Generated architecture docs are stale.\n' +
|
||||
` ${FRONTEND_DOC} no longer matches its code source (app/src/App.tsx).\n` +
|
||||
' Run `pnpm docs:generate` and commit the result.'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✓ Generated architecture docs are up to date.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (updated !== current) {
|
||||
writeFileSync(FRONTEND_DOC, updated);
|
||||
console.log(`✓ Regenerated provider-chain block in ${FRONTEND_DOC}`);
|
||||
} else {
|
||||
console.log('✓ Generated architecture docs already up to date — nothing to write.');
|
||||
}
|
||||
}
|
||||
|
||||
// Only run when invoked directly, so the pure helpers above stay unit-testable.
|
||||
if (resolve(fileURLToPath(import.meta.url)) === resolve(process.argv[1] ?? '')) {
|
||||
main();
|
||||
}
|
||||
Reference in New Issue
Block a user