Add way for infisical to switch accounts when signed into wrong account
This commit is contained in:
@@ -33,6 +33,9 @@
|
|||||||
- Local `dev` and `staging` come only from Infisical via
|
- Local `dev` and `staging` come only from Infisical via
|
||||||
`scripts/with-env`; it never falls back to `.env*`.
|
`scripts/with-env`; it never falls back to `.env*`.
|
||||||
- Run `infisical login` and `infisical init` before local development.
|
- Run `infisical login` and `infisical init` before local development.
|
||||||
|
- `scripts/export-env` enforces `.local/infisical.env` when multiple local
|
||||||
|
Infisical accounts are logged in. Put `INFISICAL_EMAIL=you@example.com` there
|
||||||
|
for this project and do not commit it.
|
||||||
- Machine-generated values belong in `.local/<env>.generated.env`; never put
|
- Machine-generated values belong in `.local/<env>.generated.env`; never put
|
||||||
the generated Convex admin key in shared Infisical.
|
the generated Convex admin key in shared Infisical.
|
||||||
- `scripts/sync-convex-env <dev|staging>` copies Authentik, GitHub App,
|
- `scripts/sync-convex-env <dev|staging>` copies Authentik, GitHub App,
|
||||||
|
|||||||
@@ -287,6 +287,35 @@ native simulator.
|
|||||||
This project is currently private, so this section is a reference for what the
|
This project is currently private, so this section is a reference for what the
|
||||||
application expects rather than public setup documentation.
|
application expects rather than public setup documentation.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Local Infisical account selection</strong></summary>
|
||||||
|
|
||||||
|
Local `dev` and `staging` commands export secrets through Infisical. Spoon runs
|
||||||
|
`scripts/infisical-account ensure` from `scripts/export-env` before exporting so
|
||||||
|
machines logged into multiple Infisical accounts do not accidentally use the
|
||||||
|
wrong organization.
|
||||||
|
|
||||||
|
If your machine has only one local Infisical account, no extra setup is needed.
|
||||||
|
If it has multiple accounts, create this ignored local file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p .local
|
||||||
|
printf "INFISICAL_EMAIL=me@gbrown.org\n" > .local/infisical.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Log into each needed account once with `infisical login`. You can inspect local
|
||||||
|
profiles without printing tokens:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
jq '.loggedInUsers[] | {email, domain}' ~/.infisical/infisical-config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`.local/infisical.env` supports only `INFISICAL_EMAIL=...` and must not be
|
||||||
|
committed. CI is unchanged; it uses injected environment files/secrets and must
|
||||||
|
not call Infisical.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<details open>
|
<details open>
|
||||||
<summary><strong>Public Next variables</strong></summary>
|
<summary><strong>Public Next variables</strong></summary>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
|
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
type TestWorkspace = {
|
||||||
|
binDir: string;
|
||||||
|
homeDir: string;
|
||||||
|
localFile: string;
|
||||||
|
projectDir: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scriptPath = fileURLToPath(
|
||||||
|
new URL('../../../../scripts/infisical-account', import.meta.url),
|
||||||
|
);
|
||||||
|
|
||||||
|
let workspaces: TestWorkspace[] = [];
|
||||||
|
|
||||||
|
const createWorkspace = async (): Promise<TestWorkspace> => {
|
||||||
|
const root = await realpathTemp();
|
||||||
|
const homeDir = path.join(root, 'home');
|
||||||
|
const projectDir = path.join(root, 'project');
|
||||||
|
const binDir = path.join(root, 'bin');
|
||||||
|
const localFile = path.join(projectDir, '.local', 'infisical.env');
|
||||||
|
|
||||||
|
await mkdir(path.join(homeDir, '.infisical'), { recursive: true });
|
||||||
|
await mkdir(path.dirname(localFile), { recursive: true });
|
||||||
|
await mkdir(binDir, { recursive: true });
|
||||||
|
|
||||||
|
const fakeInfisical = path.join(binDir, 'infisical');
|
||||||
|
await writeFile(fakeInfisical, '#!/usr/bin/env sh\nexit 0\n');
|
||||||
|
await chmod(fakeInfisical, 0o755);
|
||||||
|
|
||||||
|
const workspace = { binDir, homeDir, localFile, projectDir };
|
||||||
|
workspaces.push(workspace);
|
||||||
|
return workspace;
|
||||||
|
};
|
||||||
|
|
||||||
|
const realpathTemp = async (): Promise<string> => {
|
||||||
|
const base = path.join(tmpdir(), 'spoon-infisical-account-');
|
||||||
|
const { mkdtemp } = await import('node:fs/promises');
|
||||||
|
return mkdtemp(base);
|
||||||
|
};
|
||||||
|
|
||||||
|
const configPath = (workspace: TestWorkspace) =>
|
||||||
|
path.join(workspace.homeDir, '.infisical', 'infisical-config.json');
|
||||||
|
|
||||||
|
const writeConfig = async (
|
||||||
|
workspace: TestWorkspace,
|
||||||
|
config: Record<string, unknown> | string,
|
||||||
|
) => {
|
||||||
|
const content =
|
||||||
|
typeof config === 'string' ? config : `${JSON.stringify(config, null, 2)}\n`;
|
||||||
|
await writeFile(configPath(workspace), content);
|
||||||
|
};
|
||||||
|
|
||||||
|
const readConfig = async (
|
||||||
|
workspace: TestWorkspace,
|
||||||
|
): Promise<Record<string, unknown>> =>
|
||||||
|
JSON.parse(await readFile(configPath(workspace), 'utf8')) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
|
const envFor = (workspace: TestWorkspace): NodeJS.ProcessEnv => ({
|
||||||
|
...process.env,
|
||||||
|
HOME: workspace.homeDir,
|
||||||
|
PATH: `${workspace.binDir}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
|
||||||
|
SPOON_INFISICAL_LOCAL_FILE: workspace.localFile,
|
||||||
|
});
|
||||||
|
|
||||||
|
const runEnsure = (workspace: TestWorkspace) =>
|
||||||
|
spawnSync(scriptPath, ['ensure'], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
env: envFor(workspace),
|
||||||
|
});
|
||||||
|
|
||||||
|
const writeLocalEmail = async (workspace: TestWorkspace, emailLine: string) => {
|
||||||
|
await mkdir(path.dirname(workspace.localFile), { recursive: true });
|
||||||
|
await writeFile(workspace.localFile, `${emailLine}\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const twoAccountConfig = {
|
||||||
|
loggedInUsers: [
|
||||||
|
{ email: 'work@example.com', domain: 'https://app.infisical.com' },
|
||||||
|
{ email: 'home@example.com', domain: 'https://infisical.gbrown.org' },
|
||||||
|
],
|
||||||
|
loggedInUserEmail: 'work@example.com',
|
||||||
|
LoggedInUserDomain: 'https://app.infisical.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
workspaces = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await Promise.all(
|
||||||
|
workspaces.map((workspace) =>
|
||||||
|
rm(path.dirname(workspace.homeDir), { force: true, recursive: true }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('infisical-account', () => {
|
||||||
|
test('single account no-ops without local file', async () => {
|
||||||
|
const workspace = await createWorkspace();
|
||||||
|
await writeConfig(workspace, {
|
||||||
|
loggedInUsers: [
|
||||||
|
{ email: 'work@example.com', domain: 'https://app.infisical.com' },
|
||||||
|
],
|
||||||
|
loggedInUserEmail: 'work@example.com',
|
||||||
|
LoggedInUserDomain: 'https://app.infisical.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = runEnsure(workspace);
|
||||||
|
const config = await readConfig(workspace);
|
||||||
|
|
||||||
|
expect(result.status).toBe(0);
|
||||||
|
expect(config.loggedInUserEmail).toBe('work@example.com');
|
||||||
|
expect(config.LoggedInUserDomain).toBe('https://app.infisical.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multiple accounts require local project config', async () => {
|
||||||
|
const workspace = await createWorkspace();
|
||||||
|
await writeConfig(workspace, twoAccountConfig);
|
||||||
|
|
||||||
|
const result = runEnsure(workspace);
|
||||||
|
|
||||||
|
expect(result.status).not.toBe(0);
|
||||||
|
expect(result.stderr).toContain('.local/infisical.env');
|
||||||
|
expect(result.stderr).toContain('work@example.com');
|
||||||
|
expect(result.stderr).toContain('home@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multiple accounts switch to configured email', async () => {
|
||||||
|
const workspace = await createWorkspace();
|
||||||
|
await writeConfig(workspace, twoAccountConfig);
|
||||||
|
await writeLocalEmail(workspace, 'INFISICAL_EMAIL=home@example.com');
|
||||||
|
|
||||||
|
const result = runEnsure(workspace);
|
||||||
|
const config = await readConfig(workspace);
|
||||||
|
|
||||||
|
expect(result.status).toBe(0);
|
||||||
|
expect(config.loggedInUserEmail).toBe('home@example.com');
|
||||||
|
expect(config.LoggedInUserDomain).toBe('https://infisical.gbrown.org');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('configured email missing from local accounts fails clearly', async () => {
|
||||||
|
const workspace = await createWorkspace();
|
||||||
|
await writeConfig(workspace, twoAccountConfig);
|
||||||
|
await writeLocalEmail(workspace, 'INFISICAL_EMAIL=missing@example.com');
|
||||||
|
|
||||||
|
const result = runEnsure(workspace);
|
||||||
|
|
||||||
|
expect(result.status).not.toBe(0);
|
||||||
|
expect(result.stderr).toContain(
|
||||||
|
'not logged in locally: missing@example.com',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
'INFISICAL_EMAIL="home@example.com"',
|
||||||
|
"INFISICAL_EMAIL='home@example.com'",
|
||||||
|
])('quoted email parses correctly: %s', async (line) => {
|
||||||
|
const workspace = await createWorkspace();
|
||||||
|
await writeConfig(workspace, twoAccountConfig);
|
||||||
|
await writeLocalEmail(workspace, line);
|
||||||
|
|
||||||
|
const result = runEnsure(workspace);
|
||||||
|
const config = await readConfig(workspace);
|
||||||
|
|
||||||
|
expect(result.status).toBe(0);
|
||||||
|
expect(config.loggedInUserEmail).toBe('home@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty email fails clearly', async () => {
|
||||||
|
const workspace = await createWorkspace();
|
||||||
|
await writeConfig(workspace, twoAccountConfig);
|
||||||
|
await writeLocalEmail(workspace, 'INFISICAL_EMAIL=');
|
||||||
|
|
||||||
|
const result = runEnsure(workspace);
|
||||||
|
|
||||||
|
expect(result.status).not.toBe(0);
|
||||||
|
expect(result.stderr).toContain(
|
||||||
|
'.local/infisical.env must contain INFISICAL_EMAIL',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('corrupt config fails clearly', async () => {
|
||||||
|
const workspace = await createWorkspace();
|
||||||
|
await writeConfig(workspace, '{not-json');
|
||||||
|
|
||||||
|
const result = runEnsure(workspace);
|
||||||
|
|
||||||
|
expect(result.status).not.toBe(0);
|
||||||
|
expect(result.stderr).toContain(
|
||||||
|
'Infisical config is invalid or missing loggedInUsers',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('concurrent ensure calls do not corrupt config', async () => {
|
||||||
|
const workspace = await createWorkspace();
|
||||||
|
await writeConfig(workspace, twoAccountConfig);
|
||||||
|
await writeLocalEmail(workspace, 'INFISICAL_EMAIL=home@example.com');
|
||||||
|
|
||||||
|
const run = () =>
|
||||||
|
new Promise<{ status: number | null; stderr: string }>((resolve) => {
|
||||||
|
const child = spawn(scriptPath, ['ensure'], { env: envFor(workspace) });
|
||||||
|
let stderr = '';
|
||||||
|
child.stderr.on('data', (chunk: Buffer) => {
|
||||||
|
stderr += chunk.toString('utf8');
|
||||||
|
});
|
||||||
|
child.on('close', (status) => {
|
||||||
|
resolve({ status, stderr });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const [first, second] = await Promise.all([run(), run()]);
|
||||||
|
const config = await readConfig(workspace);
|
||||||
|
|
||||||
|
expect(first).toEqual({ status: 0, stderr: '' });
|
||||||
|
expect(second).toEqual({ status: 0, stderr: '' });
|
||||||
|
expect(config.loggedInUserEmail).toBe('home@example.com');
|
||||||
|
expect(config.LoggedInUserDomain).toBe('https://infisical.gbrown.org');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,6 @@
|
|||||||
"lib": ["ES2022", "DOM"],
|
"lib": ["ES2022", "DOM"],
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src", "eslint.config.ts", "vitest.config.ts"],
|
"include": ["src", "tests", "eslint.config.ts", "vitest.config.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ fi
|
|||||||
|
|
||||||
[ -f "$ROOT_DIR/.infisical.json" ] || { echo "export-env: run 'infisical init' in this repository." >&2; exit 1; }
|
[ -f "$ROOT_DIR/.infisical.json" ] || { echo "export-env: run 'infisical init' in this repository." >&2; exit 1; }
|
||||||
command -v infisical >/dev/null 2>&1 || { echo "export-env: Infisical CLI is required." >&2; exit 1; }
|
command -v infisical >/dev/null 2>&1 || { echo "export-env: Infisical CLI is required." >&2; exit 1; }
|
||||||
|
"$ROOT_DIR/scripts/infisical-account" ensure
|
||||||
|
|
||||||
(cd "$ROOT_DIR" && infisical export --env="$INFISICAL_ENV" --format=dotenv --silent) || {
|
(cd "$ROOT_DIR" && infisical export --env="$INFISICAL_ENV" --format=dotenv --silent) || {
|
||||||
echo "export-env: failed to export '$INFISICAL_ENV'; check login and project access." >&2
|
echo "export-env: failed to export '$INFISICAL_ENV'; check login and project access." >&2
|
||||||
|
|||||||
Executable
+177
@@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
LOCAL_FILE="${SPOON_INFISICAL_LOCAL_FILE:-$ROOT_DIR/.local/infisical.env}"
|
||||||
|
INFISICAL_DIR="${HOME}/.infisical"
|
||||||
|
CONFIG_FILE="$INFISICAL_DIR/infisical-config.json"
|
||||||
|
LOCK_FILE="$INFISICAL_DIR/.switch.lock"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
printf 'usage: infisical-account <ensure|status>\n' >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
require_command() {
|
||||||
|
local command_name="$1"
|
||||||
|
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||||
|
printf 'infisical-account: required command not found: %s\n' "$command_name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_dependencies() {
|
||||||
|
require_command infisical
|
||||||
|
require_command jq
|
||||||
|
require_command flock
|
||||||
|
}
|
||||||
|
|
||||||
|
require_config() {
|
||||||
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||||
|
printf 'infisical-account: Infisical config not found at ~/.infisical/infisical-config.json.\n' >&2
|
||||||
|
printf 'Run: infisical login\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
logged_in_user_count() {
|
||||||
|
jq -er '
|
||||||
|
if (.loggedInUsers | type) == "array" then
|
||||||
|
.loggedInUsers | length
|
||||||
|
else
|
||||||
|
error("loggedInUsers missing")
|
||||||
|
end
|
||||||
|
' "$CONFIG_FILE" 2>/dev/null || {
|
||||||
|
printf 'infisical-account: Infisical config is invalid or missing loggedInUsers.\n' >&2
|
||||||
|
printf 'Run: infisical login\n' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print_logged_in_accounts() {
|
||||||
|
jq -r '(.loggedInUsers // [])[] | " - \(.email)"' "$CONFIG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
trim() {
|
||||||
|
local value="$1"
|
||||||
|
value="${value#"${value%%[![:space:]]*}"}"
|
||||||
|
value="${value%"${value##*[![:space:]]}"}"
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
strip_matching_quotes() {
|
||||||
|
local value="$1"
|
||||||
|
case "$value" in
|
||||||
|
\"*\") value="${value#\"}"; value="${value%\"}" ;;
|
||||||
|
\'*\') value="${value#\'}"; value="${value%\'}" ;;
|
||||||
|
esac
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
read_project_email() {
|
||||||
|
if [[ ! -f "$LOCAL_FILE" ]]; then
|
||||||
|
printf 'infisical-account: multiple Infisical users are logged in, but .local/infisical.env is missing.\n\n' >&2
|
||||||
|
printf 'Create it with:\n' >&2
|
||||||
|
printf ' mkdir -p .local\n' >&2
|
||||||
|
printf ' printf "INFISICAL_EMAIL=you@example.com\\n" > .local/infisical.env\n\n' >&2
|
||||||
|
printf 'Logged-in accounts:\n' >&2
|
||||||
|
print_logged_in_accounts >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local email=""
|
||||||
|
local line key value
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
line="$(trim "$line")"
|
||||||
|
case "$line" in
|
||||||
|
''|'#'*) continue ;;
|
||||||
|
esac
|
||||||
|
key="$(trim "${line%%=*}")"
|
||||||
|
value="${line#*=}"
|
||||||
|
if [[ "$key" == "$line" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
value="$(strip_matching_quotes "$(trim "$value")")"
|
||||||
|
if [[ "$key" == "INFISICAL_EMAIL" ]]; then
|
||||||
|
email="$value"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done < "$LOCAL_FILE"
|
||||||
|
|
||||||
|
if [[ -z "$email" ]]; then
|
||||||
|
printf 'infisical-account: .local/infisical.env must contain INFISICAL_EMAIL=you@example.com.\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$email"
|
||||||
|
}
|
||||||
|
|
||||||
|
domain_for_email() {
|
||||||
|
local email="$1"
|
||||||
|
jq -er --arg email "$email" '
|
||||||
|
first((.loggedInUsers // [])[] | select(.email == $email) | .domain)
|
||||||
|
' "$CONFIG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_to_email() {
|
||||||
|
local email="$1"
|
||||||
|
local domain
|
||||||
|
domain="$(domain_for_email "$email")" || {
|
||||||
|
printf 'infisical-account: configured Infisical user is not logged in locally: %s\n' "$email" >&2
|
||||||
|
printf 'Run: infisical login\n' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp "$INFISICAL_DIR/infisical-config.XXXXXX")"
|
||||||
|
jq --arg email "$email" --arg domain "$domain" '
|
||||||
|
.loggedInUserEmail = $email
|
||||||
|
| .LoggedInUserDomain = $domain
|
||||||
|
' "$CONFIG_FILE" > "$tmp"
|
||||||
|
chmod 600 "$tmp"
|
||||||
|
mv "$tmp" "$CONFIG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_account() {
|
||||||
|
require_dependencies
|
||||||
|
require_config
|
||||||
|
|
||||||
|
(
|
||||||
|
flock 9
|
||||||
|
local count
|
||||||
|
count="$(logged_in_user_count)"
|
||||||
|
if [[ "$count" -eq 0 ]]; then
|
||||||
|
printf 'infisical-account: no local Infisical users are logged in.\n' >&2
|
||||||
|
printf 'Run: infisical login\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$count" -eq 1 ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local email
|
||||||
|
email="$(read_project_email)"
|
||||||
|
switch_to_email "$email"
|
||||||
|
) 9>"$LOCK_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
ensure_account
|
||||||
|
local count configured active
|
||||||
|
count="$(logged_in_user_count)"
|
||||||
|
configured="not required"
|
||||||
|
if [[ -f "$LOCAL_FILE" ]]; then
|
||||||
|
configured="$(read_project_email 2>/dev/null || printf 'invalid')"
|
||||||
|
fi
|
||||||
|
active="$(jq -r '.loggedInUserEmail // "unknown"' "$CONFIG_FILE")"
|
||||||
|
|
||||||
|
printf 'Infisical accounts logged in: %s\n' "$count"
|
||||||
|
printf 'Configured project email: %s\n' "$configured"
|
||||||
|
printf 'Active account after ensure: %s\n' "$active"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
ensure) ensure_account ;;
|
||||||
|
status) status ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user