codeharbor 0.1.1 → 0.1.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.
- package/.env.example +78 -0
- package/README.md +43 -0
- package/dist/cli.js +76 -14
- package/package.json +2 -1
package/.env.example
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
MATRIX_HOMESERVER=https://matrix.biglone.tech
|
|
2
|
+
MATRIX_USER_ID=@codeharbor-bot:example.com
|
|
3
|
+
MATRIX_ACCESS_TOKEN=
|
|
4
|
+
|
|
5
|
+
# Optional explicit trigger in groups; can be empty to disable prefix trigger.
|
|
6
|
+
MATRIX_COMMAND_PREFIX=!code
|
|
7
|
+
|
|
8
|
+
CODEX_BIN=codex
|
|
9
|
+
CODEX_MODEL=
|
|
10
|
+
CODEX_WORKDIR=/Users/biglone/workspace
|
|
11
|
+
CODEX_DANGEROUS_BYPASS=false
|
|
12
|
+
CODEX_EXEC_TIMEOUT_MS=600000
|
|
13
|
+
CODEX_SANDBOX_MODE=
|
|
14
|
+
CODEX_APPROVAL_POLICY=
|
|
15
|
+
# Optional extra CLI args, space separated (example: --search --no-alt-screen)
|
|
16
|
+
CODEX_EXTRA_ARGS=
|
|
17
|
+
# Optional JSON object for additional environment variables passed to codex child process.
|
|
18
|
+
CODEX_EXTRA_ENV_JSON=
|
|
19
|
+
|
|
20
|
+
# SQLite state database path.
|
|
21
|
+
STATE_DB_PATH=data/state.db
|
|
22
|
+
# Legacy JSON path for one-time migration import.
|
|
23
|
+
STATE_PATH=data/state.json
|
|
24
|
+
|
|
25
|
+
MAX_PROCESSED_EVENTS_PER_SESSION=200
|
|
26
|
+
MAX_SESSION_AGE_DAYS=30
|
|
27
|
+
MAX_SESSIONS=5000
|
|
28
|
+
REPLY_CHUNK_SIZE=3500
|
|
29
|
+
|
|
30
|
+
MATRIX_PROGRESS_UPDATES=true
|
|
31
|
+
MATRIX_PROGRESS_MIN_INTERVAL_MS=2500
|
|
32
|
+
MATRIX_TYPING_TIMEOUT_MS=10000
|
|
33
|
+
SESSION_ACTIVE_WINDOW_MINUTES=20
|
|
34
|
+
|
|
35
|
+
# Group trigger defaults.
|
|
36
|
+
GROUP_TRIGGER_ALLOW_MENTION=true
|
|
37
|
+
GROUP_TRIGGER_ALLOW_REPLY=true
|
|
38
|
+
GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW=true
|
|
39
|
+
GROUP_TRIGGER_ALLOW_PREFIX=true
|
|
40
|
+
|
|
41
|
+
# Optional room-level trigger overrides (JSON object).
|
|
42
|
+
# ROOM_TRIGGER_POLICY_JSON={"!room:example.com":{"allowMention":true,"allowReply":true,"allowActiveWindow":false,"allowPrefix":false}}
|
|
43
|
+
ROOM_TRIGGER_POLICY_JSON=
|
|
44
|
+
|
|
45
|
+
# Rate limiting / anti-abuse.
|
|
46
|
+
RATE_LIMIT_WINDOW_SECONDS=60
|
|
47
|
+
RATE_LIMIT_MAX_REQUESTS_PER_USER=20
|
|
48
|
+
RATE_LIMIT_MAX_REQUESTS_PER_ROOM=120
|
|
49
|
+
RATE_LIMIT_MAX_CONCURRENT_GLOBAL=8
|
|
50
|
+
RATE_LIMIT_MAX_CONCURRENT_PER_USER=1
|
|
51
|
+
RATE_LIMIT_MAX_CONCURRENT_PER_ROOM=4
|
|
52
|
+
|
|
53
|
+
# CLI compatibility mode (IM shell approximation of codex CLI).
|
|
54
|
+
CLI_COMPAT_MODE=false
|
|
55
|
+
CLI_COMPAT_PASSTHROUGH_EVENTS=true
|
|
56
|
+
CLI_COMPAT_PRESERVE_WHITESPACE=true
|
|
57
|
+
CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT=false
|
|
58
|
+
CLI_COMPAT_PROGRESS_THROTTLE_MS=300
|
|
59
|
+
CLI_COMPAT_FETCH_MEDIA=true
|
|
60
|
+
# Optional JSONL output path for executed prompt recording (for replay benchmarking).
|
|
61
|
+
CLI_COMPAT_RECORD_PATH=
|
|
62
|
+
|
|
63
|
+
DOCTOR_HTTP_TIMEOUT_MS=10000
|
|
64
|
+
|
|
65
|
+
# Admin API server (for config UI/backend).
|
|
66
|
+
ADMIN_BIND_HOST=127.0.0.1
|
|
67
|
+
ADMIN_PORT=8787
|
|
68
|
+
# Token protection for /api/admin/* endpoints.
|
|
69
|
+
# Strongly recommended for any non-localhost access.
|
|
70
|
+
# Required when exposing admin via reverse proxy/tunnel/public domain.
|
|
71
|
+
ADMIN_TOKEN=
|
|
72
|
+
# Optional IP allowlist (comma-separated, for example: 127.0.0.1,192.168.1.10).
|
|
73
|
+
ADMIN_IP_ALLOWLIST=
|
|
74
|
+
# Optional browser origin allowlist for CORS (comma-separated).
|
|
75
|
+
# Example: https://admin.example.com,https://ops.example.com
|
|
76
|
+
ADMIN_ALLOWED_ORIGINS=
|
|
77
|
+
|
|
78
|
+
LOG_LEVEL=info
|
package/README.md
CHANGED
|
@@ -49,6 +49,44 @@ Install globally from npm (after publish):
|
|
|
49
49
|
npm install -g codeharbor
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
Linux one-command install (creates `/opt/codeharbor`, sets ownership, installs latest package):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
curl -fsSL https://raw.githubusercontent.com/biglone/CodeHarbor/main/scripts/install-linux.sh | bash
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Linux easy mode (install + write `.env` + enable/start systemd in one run):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
curl -fsSL https://raw.githubusercontent.com/biglone/CodeHarbor/main/scripts/install-linux-easy.sh | bash -s -- \
|
|
62
|
+
--matrix-homeserver https://matrix.example.com \
|
|
63
|
+
--matrix-user-id @bot:example.com \
|
|
64
|
+
--matrix-access-token 'your-token'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Enable Admin service at install time:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
curl -fsSL https://raw.githubusercontent.com/biglone/CodeHarbor/main/scripts/install-linux-easy.sh | bash -s -- \
|
|
71
|
+
--matrix-homeserver https://matrix.example.com \
|
|
72
|
+
--matrix-user-id @bot:example.com \
|
|
73
|
+
--matrix-access-token 'your-token' \
|
|
74
|
+
--enable-admin-service \
|
|
75
|
+
--admin-token 'replace-with-strong-token'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Run local script with custom options:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
./scripts/install-linux.sh --app-dir /srv/codeharbor --package codeharbor@0.1.1 --init
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Runtime home behavior:
|
|
85
|
+
|
|
86
|
+
- By default, all `codeharbor` commands use `/opt/codeharbor` for `.env` and relative data paths.
|
|
87
|
+
- No manual `cd /opt/codeharbor` is required after installation.
|
|
88
|
+
- To use a custom runtime directory, set `CODEHARBOR_HOME` (for example `export CODEHARBOR_HOME=/srv/codeharbor`).
|
|
89
|
+
|
|
52
90
|
Install directly from GitHub:
|
|
53
91
|
|
|
54
92
|
```bash
|
|
@@ -133,6 +171,7 @@ npm install
|
|
|
133
171
|
2. Configure environment:
|
|
134
172
|
|
|
135
173
|
```bash
|
|
174
|
+
export CODEHARBOR_HOME="$(pwd)"
|
|
136
175
|
codeharbor init
|
|
137
176
|
```
|
|
138
177
|
|
|
@@ -145,6 +184,7 @@ Required values:
|
|
|
145
184
|
3. Run in dev mode:
|
|
146
185
|
|
|
147
186
|
```bash
|
|
187
|
+
export CODEHARBOR_HOME="$(pwd)"
|
|
148
188
|
npm run dev
|
|
149
189
|
```
|
|
150
190
|
|
|
@@ -152,6 +192,7 @@ npm run dev
|
|
|
152
192
|
|
|
153
193
|
```bash
|
|
154
194
|
npm run build
|
|
195
|
+
export CODEHARBOR_HOME="$(pwd)"
|
|
155
196
|
node dist/cli.js start
|
|
156
197
|
```
|
|
157
198
|
|
|
@@ -176,6 +217,8 @@ It documents:
|
|
|
176
217
|
- `codeharbor admin serve`: start admin UI + config API server
|
|
177
218
|
- `codeharbor config export`: export current config snapshot as JSON
|
|
178
219
|
- `codeharbor config import <file>`: import config snapshot JSON (supports `--dry-run`)
|
|
220
|
+
- `scripts/install-linux.sh`: Linux bootstrap installer (creates runtime dir + installs npm package)
|
|
221
|
+
- `scripts/install-linux-easy.sh`: one-shot Linux install + config + systemd auto-start
|
|
179
222
|
- `scripts/backup-config.sh`: export timestamped snapshot and keep latest N backups
|
|
180
223
|
- `scripts/install-backup-timer.sh`: install/update user-level systemd timer for automatic backups
|
|
181
224
|
- `npm run test:e2e`: run Admin UI end-to-end tests (Playwright)
|
package/dist/cli.js
CHANGED
|
@@ -24,6 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
|
+
var import_node_fs9 = __toESM(require("fs"));
|
|
28
|
+
var import_node_path11 = __toESM(require("path"));
|
|
27
29
|
var import_commander = require("commander");
|
|
28
30
|
|
|
29
31
|
// src/app.ts
|
|
@@ -45,12 +47,9 @@ var import_dotenv = __toESM(require("dotenv"));
|
|
|
45
47
|
async function runInitCommand(options = {}) {
|
|
46
48
|
const cwd = options.cwd ?? process.cwd();
|
|
47
49
|
const envPath = import_node_path.default.resolve(cwd, ".env");
|
|
48
|
-
const templatePath =
|
|
50
|
+
const templatePath = resolveInitTemplatePath(cwd);
|
|
49
51
|
const input = options.input ?? process.stdin;
|
|
50
52
|
const output = options.output ?? process.stdout;
|
|
51
|
-
if (!import_node_fs.default.existsSync(templatePath)) {
|
|
52
|
-
throw new Error(`Cannot find template file: ${templatePath}`);
|
|
53
|
-
}
|
|
54
53
|
const templateContent = import_node_fs.default.readFileSync(templatePath, "utf8");
|
|
55
54
|
const existingContent = import_node_fs.default.existsSync(envPath) ? import_node_fs.default.readFileSync(envPath, "utf8") : "";
|
|
56
55
|
const existingValues = existingContent ? import_dotenv.default.parse(existingContent) : {};
|
|
@@ -142,6 +141,18 @@ async function runInitCommand(options = {}) {
|
|
|
142
141
|
rl.close();
|
|
143
142
|
}
|
|
144
143
|
}
|
|
144
|
+
function resolveInitTemplatePath(cwd) {
|
|
145
|
+
const candidates = [
|
|
146
|
+
import_node_path.default.resolve(cwd, ".env.example"),
|
|
147
|
+
import_node_path.default.resolve(__dirname, "..", ".env.example")
|
|
148
|
+
];
|
|
149
|
+
for (const candidate of candidates) {
|
|
150
|
+
if (import_node_fs.default.existsSync(candidate)) {
|
|
151
|
+
return candidate;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`Cannot find template file. Tried: ${candidates.join(", ")}`);
|
|
155
|
+
}
|
|
145
156
|
function applyEnvOverrides(template, overrides) {
|
|
146
157
|
const lines = template.split(/\r?\n/);
|
|
147
158
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4160,7 +4171,6 @@ var import_node_fs6 = __toESM(require("fs"));
|
|
|
4160
4171
|
var import_node_path7 = __toESM(require("path"));
|
|
4161
4172
|
var import_dotenv2 = __toESM(require("dotenv"));
|
|
4162
4173
|
var import_zod = require("zod");
|
|
4163
|
-
import_dotenv2.default.config();
|
|
4164
4174
|
var configSchema = import_zod.z.object({
|
|
4165
4175
|
MATRIX_HOMESERVER: import_zod.z.string().url(),
|
|
4166
4176
|
MATRIX_USER_ID: import_zod.z.string().min(1),
|
|
@@ -4168,7 +4178,7 @@ var configSchema = import_zod.z.object({
|
|
|
4168
4178
|
MATRIX_COMMAND_PREFIX: import_zod.z.string().default("!code"),
|
|
4169
4179
|
CODEX_BIN: import_zod.z.string().default("codex"),
|
|
4170
4180
|
CODEX_MODEL: import_zod.z.string().optional(),
|
|
4171
|
-
CODEX_WORKDIR: import_zod.z.string().default(
|
|
4181
|
+
CODEX_WORKDIR: import_zod.z.string().default("."),
|
|
4172
4182
|
CODEX_DANGEROUS_BYPASS: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
4173
4183
|
CODEX_EXEC_TIMEOUT_MS: import_zod.z.string().default("600000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
4174
4184
|
CODEX_SANDBOX_MODE: import_zod.z.string().optional(),
|
|
@@ -4266,6 +4276,13 @@ var configSchema = import_zod.z.object({
|
|
|
4266
4276
|
adminAllowedOrigins: parseCsvList(v.ADMIN_ALLOWED_ORIGINS),
|
|
4267
4277
|
logLevel: v.LOG_LEVEL
|
|
4268
4278
|
}));
|
|
4279
|
+
function loadEnvFromFile(filePath = import_node_path7.default.resolve(process.cwd(), ".env"), env = process.env) {
|
|
4280
|
+
import_dotenv2.default.config({
|
|
4281
|
+
path: filePath,
|
|
4282
|
+
processEnv: env,
|
|
4283
|
+
quiet: true
|
|
4284
|
+
});
|
|
4285
|
+
}
|
|
4269
4286
|
function loadConfig(env = process.env) {
|
|
4270
4287
|
const parsed = configSchema.safeParse(env);
|
|
4271
4288
|
if (!parsed.success) {
|
|
@@ -4793,7 +4810,7 @@ async function runStartupPreflight(options = {}) {
|
|
|
4793
4810
|
code: "missing_dotenv",
|
|
4794
4811
|
check: ".env",
|
|
4795
4812
|
message: `No .env file found at ${envPath}.`,
|
|
4796
|
-
fix: "
|
|
4813
|
+
fix: 'Run "codeharbor init" to create baseline config.'
|
|
4797
4814
|
});
|
|
4798
4815
|
}
|
|
4799
4816
|
for (const key of REQUIRED_ENV_KEYS) {
|
|
@@ -4893,14 +4910,29 @@ function readEnv(env, key) {
|
|
|
4893
4910
|
return env[key]?.trim() ?? "";
|
|
4894
4911
|
}
|
|
4895
4912
|
|
|
4913
|
+
// src/runtime-home.ts
|
|
4914
|
+
var import_node_path10 = __toESM(require("path"));
|
|
4915
|
+
var DEFAULT_RUNTIME_HOME = "/opt/codeharbor";
|
|
4916
|
+
var RUNTIME_HOME_ENV_KEY = "CODEHARBOR_HOME";
|
|
4917
|
+
function resolveRuntimeHome(env = process.env) {
|
|
4918
|
+
const configured = env[RUNTIME_HOME_ENV_KEY]?.trim();
|
|
4919
|
+
if (configured) {
|
|
4920
|
+
return import_node_path10.default.resolve(configured);
|
|
4921
|
+
}
|
|
4922
|
+
return DEFAULT_RUNTIME_HOME;
|
|
4923
|
+
}
|
|
4924
|
+
|
|
4896
4925
|
// src/cli.ts
|
|
4926
|
+
var runtimeHome = null;
|
|
4897
4927
|
var program = new import_commander.Command();
|
|
4898
4928
|
program.name("codeharbor").description("Instant-messaging bridge for Codex CLI sessions").version("0.1.0");
|
|
4899
4929
|
program.command("init").description("Create or update .env via guided prompts").option("-f, --force", "overwrite existing .env without confirmation").action(async (options) => {
|
|
4900
|
-
|
|
4930
|
+
const home = ensureRuntimeHomeOrExit();
|
|
4931
|
+
await runInitCommand({ force: options.force ?? false, cwd: home });
|
|
4901
4932
|
});
|
|
4902
4933
|
program.command("start").description("Start CodeHarbor service").action(async () => {
|
|
4903
|
-
const
|
|
4934
|
+
const home = ensureRuntimeHomeOrExit();
|
|
4935
|
+
const config = await loadConfigWithPreflight("start", home);
|
|
4904
4936
|
if (!config) {
|
|
4905
4937
|
process.exitCode = 1;
|
|
4906
4938
|
return;
|
|
@@ -4919,7 +4951,8 @@ program.command("start").description("Start CodeHarbor service").action(async ()
|
|
|
4919
4951
|
});
|
|
4920
4952
|
});
|
|
4921
4953
|
program.command("doctor").description("Check codex and matrix connectivity").action(async () => {
|
|
4922
|
-
const
|
|
4954
|
+
const home = ensureRuntimeHomeOrExit();
|
|
4955
|
+
const config = await loadConfigWithPreflight("doctor", home);
|
|
4923
4956
|
if (!config) {
|
|
4924
4957
|
process.exitCode = 1;
|
|
4925
4958
|
return;
|
|
@@ -4932,6 +4965,7 @@ admin.command("serve").description("Start admin config API server").option("--ho
|
|
|
4932
4965
|
"--allow-insecure-no-token",
|
|
4933
4966
|
"allow serving admin API without ADMIN_TOKEN on non-loopback host (not recommended)"
|
|
4934
4967
|
).action(async (options) => {
|
|
4968
|
+
ensureRuntimeHomeOrExit();
|
|
4935
4969
|
const config = loadConfig();
|
|
4936
4970
|
const host = options.host?.trim() || config.adminBindHost;
|
|
4937
4971
|
const port = options.port ? parsePortOption(options.port, config.adminPort) : config.adminPort;
|
|
@@ -4962,7 +4996,8 @@ admin.command("serve").description("Start admin config API server").option("--ho
|
|
|
4962
4996
|
});
|
|
4963
4997
|
configCommand.command("export").description("Export config snapshot as JSON").option("-o, --output <path>", "write snapshot to file instead of stdout").action(async (options) => {
|
|
4964
4998
|
try {
|
|
4965
|
-
|
|
4999
|
+
const home = ensureRuntimeHomeOrExit();
|
|
5000
|
+
await runConfigExportCommand({ outputPath: options.output, cwd: home });
|
|
4966
5001
|
} catch (error) {
|
|
4967
5002
|
process.stderr.write(`Config export failed: ${formatError3(error)}
|
|
4968
5003
|
`);
|
|
@@ -4971,9 +5006,11 @@ configCommand.command("export").description("Export config snapshot as JSON").op
|
|
|
4971
5006
|
});
|
|
4972
5007
|
configCommand.command("import").description("Import config snapshot from JSON").argument("<file>", "snapshot file path").option("--dry-run", "validate snapshot without writing changes").action(async (file, options) => {
|
|
4973
5008
|
try {
|
|
5009
|
+
const home = ensureRuntimeHomeOrExit();
|
|
4974
5010
|
await runConfigImportCommand({
|
|
4975
5011
|
filePath: file,
|
|
4976
|
-
dryRun: options.dryRun ?? false
|
|
5012
|
+
dryRun: options.dryRun ?? false,
|
|
5013
|
+
cwd: home
|
|
4977
5014
|
});
|
|
4978
5015
|
} catch (error) {
|
|
4979
5016
|
process.stderr.write(`Config import failed: ${formatError3(error)}
|
|
@@ -4985,8 +5022,8 @@ if (process.argv.length <= 2) {
|
|
|
4985
5022
|
process.argv.push("start");
|
|
4986
5023
|
}
|
|
4987
5024
|
void program.parseAsync(process.argv);
|
|
4988
|
-
async function loadConfigWithPreflight(commandName) {
|
|
4989
|
-
const preflight = await runStartupPreflight();
|
|
5025
|
+
async function loadConfigWithPreflight(commandName, runtimeHomePath) {
|
|
5026
|
+
const preflight = await runStartupPreflight({ cwd: runtimeHomePath });
|
|
4990
5027
|
if (preflight.issues.length > 0) {
|
|
4991
5028
|
const report = formatPreflightReport(preflight, commandName);
|
|
4992
5029
|
if (preflight.ok) {
|
|
@@ -5006,6 +5043,31 @@ async function loadConfigWithPreflight(commandName) {
|
|
|
5006
5043
|
return null;
|
|
5007
5044
|
}
|
|
5008
5045
|
}
|
|
5046
|
+
function ensureRuntimeHomeOrExit() {
|
|
5047
|
+
if (runtimeHome) {
|
|
5048
|
+
return runtimeHome;
|
|
5049
|
+
}
|
|
5050
|
+
const home = resolveRuntimeHome();
|
|
5051
|
+
try {
|
|
5052
|
+
import_node_fs9.default.mkdirSync(home, { recursive: true });
|
|
5053
|
+
} catch (error) {
|
|
5054
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5055
|
+
process.stderr.write(`Runtime setup failed: cannot create ${home}. ${message}
|
|
5056
|
+
`);
|
|
5057
|
+
process.exit(1);
|
|
5058
|
+
}
|
|
5059
|
+
try {
|
|
5060
|
+
process.chdir(home);
|
|
5061
|
+
} catch (error) {
|
|
5062
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5063
|
+
process.stderr.write(`Runtime setup failed: cannot switch to ${home}. ${message}
|
|
5064
|
+
`);
|
|
5065
|
+
process.exit(1);
|
|
5066
|
+
}
|
|
5067
|
+
loadEnvFromFile(import_node_path11.default.resolve(home, ".env"));
|
|
5068
|
+
runtimeHome = home;
|
|
5069
|
+
return runtimeHome;
|
|
5070
|
+
}
|
|
5009
5071
|
function parsePortOption(raw, fallback) {
|
|
5010
5072
|
const parsed = Number.parseInt(raw, 10);
|
|
5011
5073
|
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeharbor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Instant-messaging bridge for Codex CLI sessions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
|
+
".env.example",
|
|
20
21
|
"README.md",
|
|
21
22
|
"LICENSE"
|
|
22
23
|
],
|