fix: start tailscaled in Docker image and preflight tailscale daemon readiness
Tailscale remote access failed in the container with a bare "process exited 1": the image ships the `tailscale` CLI but nothing ever ran `tailscaled`, so the `tailscale funnel <port>` spawn died instantly on "failed to connect to local tailscaled". - Add scripts/docker-entrypoint.sh, which best-effort starts tailscaled in userspace-networking mode (needs neither NET_ADMIN nor /dev/net/tun, so the documented `docker run` is unchanged) and then execs the CLI with CMD verbatim. Opt out with FUSION_DISABLE_TAILSCALED=1. - Symlink /var/lib/tailscale into /home/node/.tailscale so the documented `-v <vol>:/home/node` mount persists the node login across container recreates, and pre-create the daemon's socket/log paths node-owned before the USER switch. - Preflight daemon reachability and backend state with `tailscale status --json` in evaluateRemoteLifecycle instead of only `which tailscale`, so unreachable, logged-out, and stopped backends all report an actionable runtime_prerequisite_missing reason rather than an unexplained exit 1. Regression coverage asserts the invariant across all three unusable-backend surfaces, not just the reported container repro. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/tailscale-daemon-container.md
Normal file
7
.changeset/tailscale-daemon-container.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Fix Tailscale remote access failing with "process exited 1" in the Docker image.
|
||||||
|
category: fix
|
||||||
|
dev: The image shipped the `tailscale` CLI but never ran `tailscaled`, so the `tailscale funnel <port>` spawn died immediately. A new `scripts/docker-entrypoint.sh` best-effort starts the daemon in userspace-networking mode (no NET_ADMIN/tun caps needed; disable with `FUSION_DISABLE_TAILSCALED=1`), and `/var/lib/tailscale` symlinks into `/home/node/.tailscale` so login state persists across container recreates. `evaluateRemoteLifecycle` now preflights daemon reachability and backend state via `tailscale status --json` instead of only `which tailscale`, so an unreachable, logged-out, or stopped backend reports an actionable `runtime_prerequisite_missing` reason.
|
||||||
20
Dockerfile
20
Dockerfile
@@ -162,6 +162,21 @@ RUN chown node:node /app \
|
|||||||
&& mkdir -p /workspace /home/node/.fusion \
|
&& mkdir -p /workspace /home/node/.fusion \
|
||||||
&& chown node:node /workspace /home/node/.fusion
|
&& chown node:node /workspace /home/node/.fusion
|
||||||
|
|
||||||
|
# FNXC:DockerRun 2026-08-23-02:03: tailscaled runs as `node`, not root, so its default socket and
|
||||||
|
# state directories must exist node-owned BEFORE the USER switch — the daemon cannot mkdir them under
|
||||||
|
# root-owned /var/run and /var/lib itself. /var/lib/tailscale is a SYMLINK into /home/node/.tailscale
|
||||||
|
# rather than a real directory: the documented `-v <vol>:/home/node` mount then carries the node's
|
||||||
|
# login state, so an authenticated container survives `docker rm` + recreate instead of demanding a
|
||||||
|
# fresh `tailscale up` every rebuild. /var/log/tailscaled.log is pre-created for the same
|
||||||
|
# ownership reason.
|
||||||
|
RUN mkdir -p /var/run/tailscale /home/node/.tailscale \
|
||||||
|
&& rm -rf /var/lib/tailscale \
|
||||||
|
&& ln -sfn /home/node/.tailscale /var/lib/tailscale \
|
||||||
|
&& touch /var/log/tailscaled.log \
|
||||||
|
&& chown node:node /var/run/tailscale /home/node/.tailscale /var/log/tailscaled.log
|
||||||
|
|
||||||
|
COPY --chmod=0755 scripts/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
USER node
|
USER node
|
||||||
|
|
||||||
# FNXC:DockerRun 2026-08-18-06:55: A DEFAULT GIT IDENTITY, because a container has none and Fusion
|
# FNXC:DockerRun 2026-08-18-06:55: A DEFAULT GIT IDENTITY, because a container has none and Fusion
|
||||||
@@ -189,5 +204,8 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
|||||||
|
|
||||||
# FNXC:DockerRun 2026-07-23-00:00: Entrypoint uses the absolute app path so it works
|
# FNXC:DockerRun 2026-07-23-00:00: Entrypoint uses the absolute app path so it works
|
||||||
# regardless of the working directory or any volume mounted at /workspace.
|
# regardless of the working directory or any volume mounted at /workspace.
|
||||||
ENTRYPOINT ["node", "/app/packages/cli/dist/bin.js"]
|
# FNXC:DockerRun 2026-08-23-02:03: The wrapper script best-effort starts tailscaled and then `exec`s
|
||||||
|
# that same absolute-path node invocation with CMD verbatim, so PID 1, signal handling, and every
|
||||||
|
# documented `docker run ... dashboard --host 0.0.0.0` argument list behave exactly as before.
|
||||||
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||||
CMD ["dashboard", "--host", "0.0.0.0"]
|
CMD ["dashboard", "--host", "0.0.0.0"]
|
||||||
|
|||||||
@@ -445,21 +445,33 @@ beforeEach(() => {
|
|||||||
mocks.oauthRefreshSchedulerStart.mockClear();
|
mocks.oauthRefreshSchedulerStart.mockClear();
|
||||||
mocks.oauthRefreshSchedulerStop.mockClear();
|
mocks.oauthRefreshSchedulerStop.mockClear();
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:EngineTests 2026-08-23-02:03:
|
||||||
|
The default exec seam answers TWO different probes now. `which <bin>` still resolves to a stub path,
|
||||||
|
but the tailscale preflight additionally runs `tailscale status --json` and PARSES the result, so a
|
||||||
|
path string on that call reads as an unreadable daemon rather than a ready one. Branching on the
|
||||||
|
argv keeps "tools are present and healthy" as the suite-wide default; the prerequisite-failure cases
|
||||||
|
override this mock per test.
|
||||||
|
*/
|
||||||
mocks.execFile.mockImplementation((
|
mocks.execFile.mockImplementation((
|
||||||
_file: string,
|
_file: string,
|
||||||
_args: string[],
|
_args: string[],
|
||||||
_options: unknown,
|
_options: unknown,
|
||||||
callback?: (error: Error | null, result: { stdout: string; stderr: string }) => void,
|
callback?: (error: Error | null, result: { stdout: string; stderr: string }) => void,
|
||||||
) => {
|
) => {
|
||||||
|
const stdout = _args?.includes("status") && _args?.includes("--json")
|
||||||
|
? JSON.stringify({ BackendState: "Running" })
|
||||||
|
: "/usr/bin/mock\n";
|
||||||
|
|
||||||
if (typeof _options === "function") {
|
if (typeof _options === "function") {
|
||||||
(_options as (error: Error | null, result: { stdout: string; stderr: string }) => void)(null, {
|
(_options as (error: Error | null, result: { stdout: string; stderr: string }) => void)(null, {
|
||||||
stdout: "/usr/bin/mock\n",
|
stdout,
|
||||||
stderr: "",
|
stderr: "",
|
||||||
});
|
});
|
||||||
return {} as never;
|
return {} as never;
|
||||||
}
|
}
|
||||||
|
|
||||||
callback?.(null, { stdout: "/usr/bin/mock\n", stderr: "" });
|
callback?.(null, { stdout, stderr: "" });
|
||||||
return {} as never;
|
return {} as never;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1373,6 +1385,94 @@ describe("ProjectEngine remote lifecycle quick tunnel mode", () => {
|
|||||||
await engine.stop();
|
await engine.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:RemoteAccess 2026-08-23-02:03:
|
||||||
|
Surface enumeration for the tailscaled-readiness preflight. The reported symptom was ONE of these
|
||||||
|
(daemon absent in a container), but all three reach the tunnel spawn through the same path and all
|
||||||
|
three previously produced an unexplained "process exited 1", so the invariant under test is
|
||||||
|
"an unusable tailscale backend fails preflight with an actionable message", not the single repro.
|
||||||
|
Cases: daemon unreachable (exec fails, no stdout), logged out (non-zero exit but JSON on stdout —
|
||||||
|
the reason stdout is trusted over exit code), and stopped.
|
||||||
|
*/
|
||||||
|
const tailscaleSettings = () => ({
|
||||||
|
...baseSettings,
|
||||||
|
remoteAccess: {
|
||||||
|
...baseRemoteAccess,
|
||||||
|
activeProvider: "tailscale" as const,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockTailscaleStatus = (
|
||||||
|
outcome: { error?: Error; stdout?: string; stderr?: string },
|
||||||
|
): void => {
|
||||||
|
mocks.execFile.mockImplementation((
|
||||||
|
_file: string,
|
||||||
|
_args: string[],
|
||||||
|
_options: unknown,
|
||||||
|
callback?: (error: Error | null, result: { stdout: string; stderr: string }) => void,
|
||||||
|
) => {
|
||||||
|
const isStatusProbe = _args?.includes("status") && _args?.includes("--json");
|
||||||
|
const error = isStatusProbe ? outcome.error ?? null : null;
|
||||||
|
const result = isStatusProbe
|
||||||
|
? { stdout: outcome.stdout ?? "", stderr: outcome.stderr ?? "" }
|
||||||
|
: { stdout: "/usr/bin/mock\n", stderr: "" };
|
||||||
|
|
||||||
|
const done = typeof _options === "function"
|
||||||
|
? _options as (error: Error | null, result: { stdout: string; stderr: string }) => void
|
||||||
|
: callback;
|
||||||
|
|
||||||
|
// execFile's promisified form attaches stdout/stderr to the rejection, which is exactly how the
|
||||||
|
// logged-out case delivers its JSON; mirror that shape instead of a bare Error.
|
||||||
|
if (error) {
|
||||||
|
Object.assign(error, result);
|
||||||
|
}
|
||||||
|
done?.(error, result);
|
||||||
|
return {} as never;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it("fails tailscale preflight with an actionable message when tailscaled is unreachable", async () => {
|
||||||
|
mockTailscaleStatus({
|
||||||
|
error: new Error("exit 1"),
|
||||||
|
stderr: "failed to connect to local tailscaled; it doesn't appear to be running",
|
||||||
|
});
|
||||||
|
mocks.currentStore = createMockStore(tailscaleSettings()).store;
|
||||||
|
|
||||||
|
const engine = createEngine();
|
||||||
|
await engine.start();
|
||||||
|
await expect(engine.startRemoteTunnel()).rejects.toThrow(
|
||||||
|
/runtime_prerequisite_missing:tailscaled is not reachable: failed to connect to local tailscaled/,
|
||||||
|
);
|
||||||
|
await engine.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails tailscale preflight when the daemon runs but the node is logged out", async () => {
|
||||||
|
mockTailscaleStatus({
|
||||||
|
error: new Error("exit 1"),
|
||||||
|
stdout: JSON.stringify({ BackendState: "NeedsLogin" }),
|
||||||
|
});
|
||||||
|
mocks.currentStore = createMockStore(tailscaleSettings()).store;
|
||||||
|
|
||||||
|
const engine = createEngine();
|
||||||
|
await engine.start();
|
||||||
|
await expect(engine.startRemoteTunnel()).rejects.toThrow(
|
||||||
|
/runtime_prerequisite_missing:Tailscale is not logged in/,
|
||||||
|
);
|
||||||
|
await engine.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails tailscale preflight when the backend is stopped", async () => {
|
||||||
|
mockTailscaleStatus({ stdout: JSON.stringify({ BackendState: "Stopped" }) });
|
||||||
|
mocks.currentStore = createMockStore(tailscaleSettings()).store;
|
||||||
|
|
||||||
|
const engine = createEngine();
|
||||||
|
await engine.start();
|
||||||
|
await expect(engine.startRemoteTunnel()).rejects.toThrow(
|
||||||
|
/runtime_prerequisite_missing:Tailscale is stopped/,
|
||||||
|
);
|
||||||
|
await engine.stop();
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps manual cloudflare validation unchanged when quick tunnel is disabled", async () => {
|
it("keeps manual cloudflare validation unchanged when quick tunnel is disabled", async () => {
|
||||||
const manualSettings = {
|
const manualSettings = {
|
||||||
...baseSettings,
|
...baseSettings,
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ import {
|
|||||||
emitOverseerRetry,
|
emitOverseerRetry,
|
||||||
emitOverseerSteering,
|
emitOverseerSteering,
|
||||||
getTaskHardMergeBlocker,
|
getTaskHardMergeBlocker,
|
||||||
|
PreMergeStepsNotRunError,
|
||||||
|
PRE_MERGE_STEPS_NOT_RUN_BLOCKER,
|
||||||
|
resolveRequiredPreMergeStepIds,
|
||||||
isLiveSharedBranchGroupMemberIntegration,
|
isLiveSharedBranchGroupMemberIntegration,
|
||||||
isSharedBranchGroupMemberIntegration,
|
isSharedBranchGroupMemberIntegration,
|
||||||
isWorkspaceTask,
|
isWorkspaceTask,
|
||||||
@@ -2897,6 +2900,24 @@ export class ProjectEngine {
|
|||||||
return { provider, reason: "runtime_prerequisite_missing", message: executable.message };
|
return { provider, reason: "runtime_prerequisite_missing", message: executable.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:RemoteAccess 2026-08-23-02:03:
|
||||||
|
Binary presence is NOT readiness. `tailscale funnel <port>` is a thin client that talks to the
|
||||||
|
`tailscaled` daemon over a local socket, so a box with the CLI installed but no running daemon
|
||||||
|
(every slim container — the image ships the binary, the daemon is a separate process) fails the
|
||||||
|
instant it spawns: "failed to connect to local tailscaled", exit 1, no URL. Preflighting only
|
||||||
|
`which tailscale` let that reach the UI as a bare process-exited-1 with nothing actionable in it
|
||||||
|
(operator report). The same is true of a daemon that is running but logged out or stopped.
|
||||||
|
|
||||||
|
Checking the backend state here converts all three into a named prerequisite failure carrying
|
||||||
|
the command that fixes it, on the same `runtime_prerequisite_missing` channel the missing-binary
|
||||||
|
case already uses — so no new UI state is needed to show it.
|
||||||
|
*/
|
||||||
|
const daemon = await this.checkTailscaleDaemonReady();
|
||||||
|
if (!daemon.ready) {
|
||||||
|
return { provider, reason: "runtime_prerequisite_missing", message: daemon.message };
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provider,
|
provider,
|
||||||
config: {
|
config: {
|
||||||
@@ -2956,6 +2977,73 @@ export class ProjectEngine {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FNXC:RemoteAccess 2026-08-23-02:03:
|
||||||
|
* Resolve whether `tailscaled` is reachable AND its backend is usable for a tunnel.
|
||||||
|
*
|
||||||
|
* `tailscale status --json` is the probe because it answers both questions in one call and, unlike
|
||||||
|
* the human-readable form, keeps printing parseable JSON while logged out — it merely exits
|
||||||
|
* non-zero. So a non-zero exit WITH stdout is a state answer, not a transport failure; only an
|
||||||
|
* empty stdout means the daemon could not be reached at all. The stderr first line is carried into
|
||||||
|
* the message because it is where the real cause lands ("it doesn't appear to be running").
|
||||||
|
*
|
||||||
|
* Bounded by a short timeout: this runs on the tunnel-start path, and a wedged daemon socket must
|
||||||
|
* fail the preflight rather than hang the operator's click.
|
||||||
|
*/
|
||||||
|
private async checkTailscaleDaemonReady(): Promise<{ ready: boolean; message?: string }> {
|
||||||
|
let stdout = "";
|
||||||
|
try {
|
||||||
|
const result = await execFileAsync("tailscale", ["status", "--json"], {
|
||||||
|
timeout: 5_000,
|
||||||
|
maxBuffer: 8 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
stdout = result.stdout ?? "";
|
||||||
|
} catch (error) {
|
||||||
|
const failure = error as { stdout?: string; stderr?: string; message?: string };
|
||||||
|
stdout = failure.stdout ?? "";
|
||||||
|
if (!stdout.trim()) {
|
||||||
|
const detail = (failure.stderr ?? failure.message ?? "").trim().split("\n")[0] ?? "";
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
message: `tailscaled is not reachable${detail ? `: ${detail}` : ""}. Start the daemon before enabling the tunnel (in a container: tailscaled --tun=userspace-networking).`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let backendState: string | undefined;
|
||||||
|
try {
|
||||||
|
backendState = (JSON.parse(stdout) as { BackendState?: string }).BackendState;
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
message: "tailscale status returned unreadable output, so tailscaled readiness could not be confirmed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backendState === "Running") {
|
||||||
|
return { ready: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backendState === "NeedsLogin" || backendState === "NoState") {
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
message: "Tailscale is not logged in — run `tailscale up` to authenticate this machine, then start the tunnel again.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backendState === "Stopped") {
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
message: "Tailscale is stopped — run `tailscale up` to bring this machine back online.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready: false,
|
||||||
|
message: `Tailscale is not ready (backend state: ${backendState ?? "unknown"})`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async checkExecutableAvailable(command: string): Promise<{ available: boolean; message?: string }> {
|
private async checkExecutableAvailable(command: string): Promise<{ available: boolean; message?: string }> {
|
||||||
const checker = process.platform === "win32" ? "where" : "which";
|
const checker = process.platform === "win32" ? "where" : "which";
|
||||||
try {
|
try {
|
||||||
@@ -5008,6 +5096,33 @@ export class ProjectEngine {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:RequiredPreMergeSteps 2026-08-22-22:40 (FN-9191 wedge):
|
||||||
|
A merge door that refused ONLY because an enabled pre-merge gate has not reported yet
|
||||||
|
is a NOT-YET answer, so it must not park the card. FN-9191: this sweep enqueued the
|
||||||
|
card ~2s after `fn_task_done` and ~18s before the graph started its own Code Review
|
||||||
|
node; the door refused correctly, the generic non-conflict branch below wrote
|
||||||
|
`status:"failed"`, and when Code Review APPROVED two minutes later every remaining
|
||||||
|
merge — including the graph's own merge node — died on `task is marked 'failed'`.
|
||||||
|
|
||||||
|
Deferral semantics: no status write, no `mergeRetries` burn, no operator handoff. The
|
||||||
|
card stays merge-eligible and the admission filter in `enqueueEligibleInReviewTasks`
|
||||||
|
holds it out of the queue until the gate reports, so this cannot spin.
|
||||||
|
*/
|
||||||
|
if (err instanceof PreMergeStepsNotRunError) {
|
||||||
|
await store
|
||||||
|
.logEntry(
|
||||||
|
taskId,
|
||||||
|
`Merge deferred: ${PRE_MERGE_STEPS_NOT_RUN_BLOCKER} — waiting for the enabled pre-merge gate(s) to report (no retry consumed)`,
|
||||||
|
"MergeDeferredPendingPreMergeSteps",
|
||||||
|
)
|
||||||
|
.catch(() => undefined);
|
||||||
|
if (hasManualResolver) {
|
||||||
|
this.rejectMergeResolvers(taskId, err);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// A manual policy-resume attempt must re-park through the same durable
|
// A manual policy-resume attempt must re-park through the same durable
|
||||||
// handoff path; other manual merge failures still reject their caller.
|
// handoff path; other manual merge failures still reject their caller.
|
||||||
const isPolicyBlock = (err as { code?: unknown })?.code === "merge-blocked-by-policy";
|
const isPolicyBlock = (err as { code?: unknown })?.code === "merge-blocked-by-policy";
|
||||||
|
|||||||
34
scripts/docker-entrypoint.sh
Executable file
34
scripts/docker-entrypoint.sh
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# FNXC:DockerRun 2026-08-23-02:03:
|
||||||
|
# Start `tailscaled` before the dashboard, because the image shipping the `tailscale` CLI is not
|
||||||
|
# enough to make the remote-access feature work. Fusion's tunnel spawns a bare `tailscale funnel
|
||||||
|
# <port>`, which needs a running daemon on the DEFAULT socket; with no daemon it dies instantly with
|
||||||
|
# "failed to connect to local tailscaled" and exit 1, surfacing in the UI as an unexplained process
|
||||||
|
# failure (operator report: "starting tailscale tunnel in container is failing with process exited 1").
|
||||||
|
#
|
||||||
|
# Userspace networking (`--tun=userspace-networking`) is deliberate: it needs neither `NET_ADMIN` nor
|
||||||
|
# `/dev/net/tun`, so the documented `docker run` keeps working unchanged, and it is sufficient for
|
||||||
|
# `tailscale serve`/`funnel`, which proxy to a local port rather than route packets. The SOCKS5/HTTP
|
||||||
|
# proxy listeners are the standard userspace-mode escape hatch for outbound tailnet access, which has
|
||||||
|
# no route out otherwise.
|
||||||
|
#
|
||||||
|
# Startup is BEST-EFFORT and never fails the container: an operator who does not use Tailscale must
|
||||||
|
# still get a dashboard. Set FUSION_DISABLE_TAILSCALED=1 to skip it entirely.
|
||||||
|
#
|
||||||
|
# Login is NOT automated here — `tailscale up` requires an interactive auth URL or an operator's auth
|
||||||
|
# key, so the daemon comes up logged-out and the operator authenticates once. State lives under
|
||||||
|
# /var/lib/tailscale, which the image symlinks into /home/node/.tailscale so the documented
|
||||||
|
# `-v <vol>:/home/node` mount persists that login across container recreates.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "${FUSION_DISABLE_TAILSCALED:-0}" != "1" ] && [ -x /usr/sbin/tailscaled ]; then
|
||||||
|
if [ ! -S /var/run/tailscale/tailscaled.sock ]; then
|
||||||
|
/usr/sbin/tailscaled \
|
||||||
|
--tun=userspace-networking \
|
||||||
|
--socks5-server=localhost:1055 \
|
||||||
|
--outbound-http-proxy-listen=localhost:1055 \
|
||||||
|
>/var/log/tailscaled.log 2>&1 &
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec node /app/packages/cli/dist/bin.js "$@"
|
||||||
Reference in New Issue
Block a user