@envmanager-cli/cli 0.1.8 → 0.1.10
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/dist/bin/envmanager.js +252 -37
- package/dist/bin/envmanager.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +18 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/envmanager.js
CHANGED
|
@@ -464,6 +464,147 @@ import { resolve } from "path";
|
|
|
464
464
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
465
465
|
import { join as join2, dirname } from "path";
|
|
466
466
|
import { z } from "zod";
|
|
467
|
+
|
|
468
|
+
// src/lib/formatters.ts
|
|
469
|
+
import * as yaml from "js-yaml";
|
|
470
|
+
var EXPORT_FORMATS = [
|
|
471
|
+
"dotenv",
|
|
472
|
+
"docker-compose",
|
|
473
|
+
"k8s-secret",
|
|
474
|
+
"k8s-configmap",
|
|
475
|
+
"vercel",
|
|
476
|
+
"railway",
|
|
477
|
+
"render"
|
|
478
|
+
];
|
|
479
|
+
function sanitizeK8sName(name) {
|
|
480
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").substring(0, 63);
|
|
481
|
+
}
|
|
482
|
+
function toDotEnv(variables) {
|
|
483
|
+
return variables.map((v) => {
|
|
484
|
+
const needsQuotes = /[\s='"#\n\r]/.test(v.value);
|
|
485
|
+
const value = needsQuotes ? `"${v.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"` : v.value;
|
|
486
|
+
return `${v.key}=${value}`;
|
|
487
|
+
}).join("\n");
|
|
488
|
+
}
|
|
489
|
+
function toDockerCompose(variables) {
|
|
490
|
+
const envData = {};
|
|
491
|
+
for (const v of variables) {
|
|
492
|
+
envData[v.key] = v.value;
|
|
493
|
+
}
|
|
494
|
+
const snippet = {
|
|
495
|
+
services: {
|
|
496
|
+
app: {
|
|
497
|
+
environment: envData
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
const yamlOutput = yaml.dump(snippet, { lineWidth: -1, quotingType: '"' });
|
|
502
|
+
return `# Docker Compose environment section
|
|
503
|
+
# Copy this into your docker-compose.yml and replace 'app' with your service name
|
|
504
|
+
${yamlOutput}`;
|
|
505
|
+
}
|
|
506
|
+
function toKubernetesSecret(variables, config) {
|
|
507
|
+
const data = {};
|
|
508
|
+
for (const v of variables) {
|
|
509
|
+
data[v.key] = Buffer.from(v.value).toString("base64");
|
|
510
|
+
}
|
|
511
|
+
const manifest = {
|
|
512
|
+
apiVersion: "v1",
|
|
513
|
+
kind: "Secret",
|
|
514
|
+
metadata: {
|
|
515
|
+
name: config.name,
|
|
516
|
+
namespace: config.namespace
|
|
517
|
+
},
|
|
518
|
+
type: "Opaque",
|
|
519
|
+
data
|
|
520
|
+
};
|
|
521
|
+
return yaml.dump(manifest, { lineWidth: -1, quotingType: '"' });
|
|
522
|
+
}
|
|
523
|
+
function toKubernetesConfigMap(variables, config) {
|
|
524
|
+
const data = {};
|
|
525
|
+
for (const v of variables) {
|
|
526
|
+
data[v.key] = v.value;
|
|
527
|
+
}
|
|
528
|
+
const manifest = {
|
|
529
|
+
apiVersion: "v1",
|
|
530
|
+
kind: "ConfigMap",
|
|
531
|
+
metadata: {
|
|
532
|
+
name: config.name,
|
|
533
|
+
namespace: config.namespace
|
|
534
|
+
},
|
|
535
|
+
data
|
|
536
|
+
};
|
|
537
|
+
return yaml.dump(manifest, { lineWidth: -1, quotingType: '"' });
|
|
538
|
+
}
|
|
539
|
+
function toVercelCLI(variables) {
|
|
540
|
+
if (variables.length === 0) return "# No variables to export";
|
|
541
|
+
const header = `# Vercel CLI commands to add environment variables
|
|
542
|
+
# Run these commands in your project directory
|
|
543
|
+
# Docs: https://vercel.com/docs/cli/env
|
|
544
|
+
# Note: You may need to run 'vercel login' first
|
|
545
|
+
|
|
546
|
+
`;
|
|
547
|
+
const commands = variables.map((v) => {
|
|
548
|
+
if (v.value.includes("\n")) {
|
|
549
|
+
const escapedValue = v.value.replace(/'/g, "'\\''");
|
|
550
|
+
return `vercel env add ${v.key} production << 'EOF'
|
|
551
|
+
${escapedValue}
|
|
552
|
+
EOF`;
|
|
553
|
+
} else {
|
|
554
|
+
const escapedValue = v.value.replace(/'/g, "'\\''");
|
|
555
|
+
return `echo '${escapedValue}' | vercel env add ${v.key} production`;
|
|
556
|
+
}
|
|
557
|
+
}).join("\n\n");
|
|
558
|
+
return header + commands;
|
|
559
|
+
}
|
|
560
|
+
function toRailwayCLI(variables) {
|
|
561
|
+
if (variables.length === 0) return "# No variables to export";
|
|
562
|
+
const header = `# Railway CLI commands to set environment variables
|
|
563
|
+
# Run these commands in your project directory
|
|
564
|
+
# Docs: https://docs.railway.app/reference/cli-api#variables-set
|
|
565
|
+
# Note: You may need to run 'railway login' first
|
|
566
|
+
|
|
567
|
+
`;
|
|
568
|
+
const commands = variables.map((v) => {
|
|
569
|
+
const escapedValue = v.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
570
|
+
return `railway variables set ${v.key}="${escapedValue}"`;
|
|
571
|
+
}).join("\n");
|
|
572
|
+
return header + commands;
|
|
573
|
+
}
|
|
574
|
+
function toRenderCLI(variables) {
|
|
575
|
+
if (variables.length === 0) return "# No variables to export";
|
|
576
|
+
const header = `# Render CLI commands to set environment variables
|
|
577
|
+
# Run these commands in your project directory
|
|
578
|
+
# Docs: https://render.com/docs/cli
|
|
579
|
+
# Note: You may need to authenticate first
|
|
580
|
+
|
|
581
|
+
`;
|
|
582
|
+
const commands = variables.map((v) => {
|
|
583
|
+
const escapedValue = v.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
584
|
+
return `render env:set ${v.key}="${escapedValue}"`;
|
|
585
|
+
}).join("\n");
|
|
586
|
+
return header + commands;
|
|
587
|
+
}
|
|
588
|
+
function formatVariables(variables, format, k8sConfig) {
|
|
589
|
+
switch (format) {
|
|
590
|
+
case "dotenv":
|
|
591
|
+
return toDotEnv(variables);
|
|
592
|
+
case "docker-compose":
|
|
593
|
+
return toDockerCompose(variables);
|
|
594
|
+
case "k8s-secret":
|
|
595
|
+
return toKubernetesSecret(variables, k8sConfig ?? { name: "app-secrets", namespace: "default" });
|
|
596
|
+
case "k8s-configmap":
|
|
597
|
+
return toKubernetesConfigMap(variables, k8sConfig ?? { name: "app-config", namespace: "default" });
|
|
598
|
+
case "vercel":
|
|
599
|
+
return toVercelCLI(variables);
|
|
600
|
+
case "railway":
|
|
601
|
+
return toRailwayCLI(variables);
|
|
602
|
+
case "render":
|
|
603
|
+
return toRenderCLI(variables);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/lib/config.ts
|
|
467
608
|
var ConfigSchema = z.object({
|
|
468
609
|
project_id: z.string().uuid().optional(),
|
|
469
610
|
project_name: z.string().optional(),
|
|
@@ -471,7 +612,10 @@ var ConfigSchema = z.object({
|
|
|
471
612
|
environment_id: z.string().uuid().optional(),
|
|
472
613
|
organization_id: z.string().uuid().optional(),
|
|
473
614
|
output: z.string().default(".env"),
|
|
474
|
-
api_url: z.string().url().optional()
|
|
615
|
+
api_url: z.string().url().optional(),
|
|
616
|
+
format: z.enum(EXPORT_FORMATS).optional(),
|
|
617
|
+
k8s_namespace: z.string().optional(),
|
|
618
|
+
k8s_name: z.string().optional()
|
|
475
619
|
});
|
|
476
620
|
var CONFIG_FILENAMES = ["envmanager.json", ".envmanagerrc"];
|
|
477
621
|
function findConfigFile(startDir = process.cwd()) {
|
|
@@ -741,17 +885,23 @@ function resolveAll(variables) {
|
|
|
741
885
|
}
|
|
742
886
|
|
|
743
887
|
// src/commands/pull.ts
|
|
744
|
-
var pullCommand = new Command4("pull").description("Pull environment variables from EnvManager to local .env file").option("--org <name>", "Organization name (required if you belong to multiple)").option("-e, --environment <name>", 'Environment name (default: from config or "development")').option("-p, --project <id>", "Project ID (default: from config)").option("-o, --output <file>", "Output file path (default: .env)").option("--no-secrets", "Exclude secret values (will be empty)").option("-f, --force", "Overwrite existing file without prompting").option("-r, --resolve-references", "Resolve ${VAR} references to their values").option("-F, --include-fallbacks", "Include fallback values for empty variables").option("-s, --show-sources", "Show value source as inline comments").action(async (options) => {
|
|
888
|
+
var pullCommand = new Command4("pull").description("Pull environment variables from EnvManager to local .env file").option("--org <name>", "Organization name (required if you belong to multiple)").option("-e, --environment <name>", 'Environment name (default: from config or "development")').option("-p, --project <id>", "Project ID (default: from config)").option("-o, --output <file>", "Output file path (default: .env)").option("--no-secrets", "Exclude secret values (will be empty)").option("-f, --force", "Overwrite existing file without prompting").option("-r, --resolve-references", "Resolve ${VAR} references to their values").option("-F, --include-fallbacks", "Include fallback values for empty variables").option("-s, --show-sources", "Show value source as inline comments").option("--format <type>", `Export format (${EXPORT_FORMATS.join(", ")})`).option("--k8s-namespace <ns>", 'Kubernetes namespace (default: "default")').option("--k8s-name <name>", "Kubernetes resource name").action(async (options) => {
|
|
745
889
|
const spinner = ora3("Connecting to EnvManager...").start();
|
|
746
890
|
try {
|
|
747
891
|
const config = loadConfig();
|
|
748
892
|
const projectInput = options.project || config?.project_id;
|
|
749
893
|
const envName = options.environment || config?.environment || "development";
|
|
750
|
-
const outputFile = resolve(options.output || ".env");
|
|
894
|
+
const outputFile = resolve(options.output || config?.output || ".env");
|
|
751
895
|
const includeSecrets = options.secrets !== false;
|
|
752
896
|
const shouldResolve = options.resolveReferences === true;
|
|
753
897
|
const shouldFallback = options.includeFallbacks === true;
|
|
754
898
|
const shouldShowSources = options.showSources === true;
|
|
899
|
+
const formatInput = options.format || config?.format || "dotenv";
|
|
900
|
+
if (!EXPORT_FORMATS.includes(formatInput)) {
|
|
901
|
+
spinner.fail(`Invalid format "${formatInput}". Valid formats: ${EXPORT_FORMATS.join(", ")}`);
|
|
902
|
+
process.exit(1);
|
|
903
|
+
}
|
|
904
|
+
const format = formatInput;
|
|
755
905
|
if (!projectInput) {
|
|
756
906
|
spinner.fail("No project specified");
|
|
757
907
|
console.log(chalk4.yellow("\nSpecify a project with --project <id-or-name> or create envmanager.json"));
|
|
@@ -828,39 +978,63 @@ File ${outputFile} already exists.`));
|
|
|
828
978
|
const resolved = resolveAll(inputs);
|
|
829
979
|
resolvedMap = new Map(resolved.map((r) => [r.key, r]));
|
|
830
980
|
}
|
|
831
|
-
spinner.text =
|
|
832
|
-
const
|
|
981
|
+
spinner.text = `Writing ${format} output...`;
|
|
982
|
+
const exportVars = vars.map((v) => {
|
|
833
983
|
let value;
|
|
834
|
-
let source = null;
|
|
835
984
|
if (resolvedMap) {
|
|
836
|
-
|
|
837
|
-
value = resolved.resolvedValue;
|
|
838
|
-
source = resolved.source;
|
|
985
|
+
value = resolvedMap.get(v.key).resolvedValue;
|
|
839
986
|
} else if (shouldFallback && (!v.value || v.value === "") && v.fallback_value) {
|
|
840
987
|
value = v.fallback_value;
|
|
841
|
-
source = "fallback";
|
|
842
988
|
} else {
|
|
843
989
|
value = v.value || "";
|
|
844
990
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
991
|
+
return { key: v.key, value, isSecret: v.is_secret };
|
|
992
|
+
});
|
|
993
|
+
let content;
|
|
994
|
+
if (format === "dotenv" && (shouldShowSources || shouldResolve)) {
|
|
995
|
+
content = vars.map((v) => {
|
|
996
|
+
let value;
|
|
997
|
+
let source = null;
|
|
998
|
+
if (resolvedMap) {
|
|
999
|
+
const resolved = resolvedMap.get(v.key);
|
|
1000
|
+
value = resolved.resolvedValue;
|
|
1001
|
+
source = resolved.source;
|
|
1002
|
+
} else if (shouldFallback && (!v.value || v.value === "") && v.fallback_value) {
|
|
1003
|
+
value = v.fallback_value;
|
|
1004
|
+
source = "fallback";
|
|
1005
|
+
} else {
|
|
1006
|
+
value = v.value || "";
|
|
1007
|
+
}
|
|
1008
|
+
const needsQuotes = value.includes(" ") || value.includes("\n") || value.includes('"');
|
|
1009
|
+
const formattedValue = needsQuotes ? `"${value.replace(/"/g, '\\"')}"` : value;
|
|
1010
|
+
let line = `${v.key}=${formattedValue}`;
|
|
1011
|
+
if (shouldShowSources && resolvedMap) {
|
|
1012
|
+
const resolved = resolvedMap.get(v.key);
|
|
1013
|
+
if (resolved.references.length > 0 && resolved.source !== "empty") {
|
|
1014
|
+
line += ` # resolved from ${resolved.rawValue ?? v.value ?? ""}`;
|
|
1015
|
+
} else if (resolved.source === "fallback") {
|
|
1016
|
+
line += ` # fallback value`;
|
|
1017
|
+
}
|
|
1018
|
+
} else if (shouldShowSources && source === "fallback") {
|
|
853
1019
|
line += ` # fallback value`;
|
|
854
1020
|
}
|
|
855
|
-
|
|
856
|
-
|
|
1021
|
+
return line;
|
|
1022
|
+
}).join("\n");
|
|
1023
|
+
} else {
|
|
1024
|
+
let k8sConfig;
|
|
1025
|
+
if (format === "k8s-secret" || format === "k8s-configmap") {
|
|
1026
|
+
const defaultName = format === "k8s-secret" ? "app-secrets" : "app-config";
|
|
1027
|
+
k8sConfig = {
|
|
1028
|
+
name: sanitizeK8sName(options.k8sName || config?.k8s_name || defaultName),
|
|
1029
|
+
namespace: options.k8sNamespace || config?.k8s_namespace || "default"
|
|
1030
|
+
};
|
|
857
1031
|
}
|
|
858
|
-
|
|
859
|
-
}
|
|
860
|
-
writeFileSync2(outputFile,
|
|
1032
|
+
content = formatVariables(exportVars, format, k8sConfig);
|
|
1033
|
+
}
|
|
1034
|
+
writeFileSync2(outputFile, content + "\n");
|
|
861
1035
|
const secretCount = vars.filter((v) => v.is_secret).length;
|
|
862
1036
|
const plainCount = vars.length - secretCount;
|
|
863
|
-
spinner.succeed(`Pulled ${variables.length} variables to ${outputFile}`);
|
|
1037
|
+
spinner.succeed(`Pulled ${variables.length} variables to ${outputFile} (${format})`);
|
|
864
1038
|
console.log(chalk4.gray(` ${plainCount} plain, ${secretCount} secrets`));
|
|
865
1039
|
client.rpc("log_variable_access", {
|
|
866
1040
|
p_environment_id: environmentId,
|
|
@@ -1509,6 +1683,12 @@ async function subscribeToVariableChanges(environmentId, onEvent, onStatus) {
|
|
|
1509
1683
|
channel.on("broadcast", { event: "variable_change" }, (payload) => {
|
|
1510
1684
|
onEvent(payload.payload);
|
|
1511
1685
|
});
|
|
1686
|
+
channel.on("broadcast", { event: "*" }, (payload) => {
|
|
1687
|
+
const data = payload.payload;
|
|
1688
|
+
if (data?.action && data?.key && data?.environment_id) {
|
|
1689
|
+
onEvent(data);
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1512
1692
|
channel.on("system", { event: "*" }, (status) => {
|
|
1513
1693
|
if (status.event === "connected") {
|
|
1514
1694
|
reconnectAttempts = 0;
|
|
@@ -1750,6 +1930,38 @@ var devCommand = new Command9("dev").description("Start real-time sync daemon -
|
|
|
1750
1930
|
spinner.text = "Connecting to realtime...";
|
|
1751
1931
|
let fileWatcher = null;
|
|
1752
1932
|
let isPaused = false;
|
|
1933
|
+
let lastRemoteKeys = null;
|
|
1934
|
+
async function syncRemoteToLocal(silent = false) {
|
|
1935
|
+
const updatedVariables = await fetchAllVariables(environmentId, true);
|
|
1936
|
+
const currentLocal = /* @__PURE__ */ new Map();
|
|
1937
|
+
if (existsSync7(outputFile)) {
|
|
1938
|
+
const content = readFileSync6(outputFile, "utf-8");
|
|
1939
|
+
Object.entries(parseDotenv3(content)).forEach(([k, v]) => {
|
|
1940
|
+
currentLocal.set(k, v);
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
const newMerged = mergeWithRemote(currentLocal, updatedVariables, strategy);
|
|
1944
|
+
const remoteKeySig = [...newMerged.entries()].sort().map(([k, v]) => `${k}=${v}`).join("\n");
|
|
1945
|
+
if (remoteKeySig === lastRemoteKeys) return false;
|
|
1946
|
+
lastRemoteKeys = remoteKeySig;
|
|
1947
|
+
if (!silent) {
|
|
1948
|
+
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
1949
|
+
for (const key of currentLocal.keys()) {
|
|
1950
|
+
if (!newMerged.has(key)) {
|
|
1951
|
+
console.log(chalk10.red(`[${timestamp}] - ${key}`));
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
for (const [key] of newMerged) {
|
|
1955
|
+
if (!currentLocal.has(key)) {
|
|
1956
|
+
console.log(chalk10.green(`[${timestamp}] + ${key}`));
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
writeFileSync3(outputFile, formatEnvFile(newMerged));
|
|
1961
|
+
return true;
|
|
1962
|
+
}
|
|
1963
|
+
const initialKeys = [...merged.entries()].sort().map(([k, v]) => `${k}=${v}`).join("\n");
|
|
1964
|
+
lastRemoteKeys = initialKeys;
|
|
1753
1965
|
const subscription = await subscribeToVariableChanges(
|
|
1754
1966
|
environmentId,
|
|
1755
1967
|
async (event) => {
|
|
@@ -1772,16 +1984,7 @@ var devCommand = new Command9("dev").description("Start real-time sync daemon -
|
|
|
1772
1984
|
}
|
|
1773
1985
|
isPaused = true;
|
|
1774
1986
|
try {
|
|
1775
|
-
|
|
1776
|
-
const currentLocal = /* @__PURE__ */ new Map();
|
|
1777
|
-
if (existsSync7(outputFile)) {
|
|
1778
|
-
const content = readFileSync6(outputFile, "utf-8");
|
|
1779
|
-
Object.entries(parseDotenv3(content)).forEach(([k, v]) => {
|
|
1780
|
-
currentLocal.set(k, v);
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
const newMerged = mergeWithRemote(currentLocal, updatedVariables, strategy);
|
|
1784
|
-
writeFileSync3(outputFile, formatEnvFile(newMerged));
|
|
1987
|
+
await syncRemoteToLocal(true);
|
|
1785
1988
|
} finally {
|
|
1786
1989
|
isPaused = false;
|
|
1787
1990
|
}
|
|
@@ -1828,6 +2031,17 @@ Realtime error: ${message || ""}`));
|
|
|
1828
2031
|
console.log(chalk10.gray("Watching for changes. Press Ctrl+C to stop."));
|
|
1829
2032
|
console.log("");
|
|
1830
2033
|
let isCleaningUp = false;
|
|
2034
|
+
const POLL_INTERVAL_MS = 5e3;
|
|
2035
|
+
const pollInterval = setInterval(async () => {
|
|
2036
|
+
if (isPaused) return;
|
|
2037
|
+
isPaused = true;
|
|
2038
|
+
try {
|
|
2039
|
+
await syncRemoteToLocal();
|
|
2040
|
+
} catch {
|
|
2041
|
+
} finally {
|
|
2042
|
+
isPaused = false;
|
|
2043
|
+
}
|
|
2044
|
+
}, POLL_INTERVAL_MS);
|
|
1831
2045
|
const TOKEN_REFRESH_INTERVAL_MS = 30 * 60 * 1e3;
|
|
1832
2046
|
const refreshInterval = setInterval(async () => {
|
|
1833
2047
|
try {
|
|
@@ -1842,6 +2056,7 @@ Realtime error: ${message || ""}`));
|
|
|
1842
2056
|
}
|
|
1843
2057
|
isCleaningUp = true;
|
|
1844
2058
|
console.log(chalk10.gray("\nStopping dev mode..."));
|
|
2059
|
+
clearInterval(pollInterval);
|
|
1845
2060
|
clearInterval(refreshInterval);
|
|
1846
2061
|
fileWatcher?.stop();
|
|
1847
2062
|
subscription.unsubscribe().catch(() => {
|
|
@@ -1962,9 +2177,9 @@ function validateAgainstTemplate(template, env) {
|
|
|
1962
2177
|
}
|
|
1963
2178
|
|
|
1964
2179
|
// src/lib/template-yaml.ts
|
|
1965
|
-
import
|
|
2180
|
+
import yaml2 from "js-yaml";
|
|
1966
2181
|
function parseYamlTemplate(content) {
|
|
1967
|
-
const parsed =
|
|
2182
|
+
const parsed = yaml2.load(content);
|
|
1968
2183
|
if (!parsed || typeof parsed !== "object") {
|
|
1969
2184
|
throw new Error("Invalid YAML template: must be an object");
|
|
1970
2185
|
}
|
|
@@ -2109,7 +2324,7 @@ function generateYamlTemplate(variables, options = {}) {
|
|
|
2109
2324
|
vars[v.key] = varConfig;
|
|
2110
2325
|
}
|
|
2111
2326
|
template.variables = vars;
|
|
2112
|
-
return
|
|
2327
|
+
return yaml2.dump(template, { lineWidth: -1, quotingType: '"' });
|
|
2113
2328
|
}
|
|
2114
2329
|
|
|
2115
2330
|
// src/commands/init.ts
|