@alecrust/workbox 0.4.1
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/CHANGELOG.md +1 -0
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/package.json +59 -0
- package/src/bootstrap/runner.ts +89 -0
- package/src/bootstrap/steps/index.ts +6 -0
- package/src/cli/args.ts +144 -0
- package/src/cli/help.ts +42 -0
- package/src/cli.ts +145 -0
- package/src/commands/dev.ts +90 -0
- package/src/commands/exec.ts +67 -0
- package/src/commands/index.ts +23 -0
- package/src/commands/list.ts +42 -0
- package/src/commands/new.ts +73 -0
- package/src/commands/parse.ts +14 -0
- package/src/commands/prune.ts +30 -0
- package/src/commands/rm.ts +64 -0
- package/src/commands/setup.ts +41 -0
- package/src/commands/status.ts +42 -0
- package/src/commands/types.ts +30 -0
- package/src/core/config.ts +317 -0
- package/src/core/git.ts +352 -0
- package/src/core/path.ts +103 -0
- package/src/core/paths.ts +23 -0
- package/src/core/process.ts +52 -0
- package/src/core/repo.ts +44 -0
- package/src/provision/runner.ts +204 -0
- package/src/ui/errors.ts +23 -0
- package/src/ui/log.ts +32 -0
package/src/core/path.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
2
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const isSubpath = (candidatePath: string, basePath: string): boolean => {
|
|
5
|
+
const base = resolve(basePath);
|
|
6
|
+
const candidate = resolve(candidatePath);
|
|
7
|
+
if (candidate === base) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
const baseWithSep = base.endsWith(sep) ? base : `${base}${sep}`;
|
|
11
|
+
return candidate.startsWith(baseWithSep);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const realpathOrResolve = async (path: string): Promise<string> => {
|
|
15
|
+
try {
|
|
16
|
+
return await realpath(path);
|
|
17
|
+
} catch {
|
|
18
|
+
return resolve(path);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const checkSegmentsWithinRoot = async (
|
|
23
|
+
rootResolved: string,
|
|
24
|
+
rootReal: string,
|
|
25
|
+
rel: string,
|
|
26
|
+
label: string
|
|
27
|
+
): Promise<{ ok: true } | { ok: false; reason: string }> => {
|
|
28
|
+
let cursor = rootResolved;
|
|
29
|
+
const segments = rel.split(sep).filter((segment) => segment.length > 0);
|
|
30
|
+
|
|
31
|
+
for (const segment of segments) {
|
|
32
|
+
cursor = join(cursor, segment);
|
|
33
|
+
try {
|
|
34
|
+
const stat = await lstat(cursor);
|
|
35
|
+
if (stat.isSymbolicLink()) {
|
|
36
|
+
const linkTarget = await realpathOrResolve(cursor);
|
|
37
|
+
if (!isSubpath(linkTarget, rootReal)) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
reason: `${label} escapes repo root via symlink (${cursor} -> ${linkTarget}).`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (
|
|
46
|
+
error instanceof Error &&
|
|
47
|
+
"code" in error &&
|
|
48
|
+
(error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
49
|
+
)
|
|
50
|
+
return { ok: true };
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { ok: true };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const checkPathWithinRoot = async (input: {
|
|
59
|
+
rootDir: string;
|
|
60
|
+
candidatePath: string;
|
|
61
|
+
label: string;
|
|
62
|
+
}): Promise<{ ok: true } | { ok: false; reason: string }> => {
|
|
63
|
+
const rootResolved = resolve(input.rootDir);
|
|
64
|
+
const rootReal = await realpathOrResolve(rootResolved);
|
|
65
|
+
const candidateResolved = resolve(input.candidatePath);
|
|
66
|
+
|
|
67
|
+
if (!isSubpath(candidateResolved, rootResolved)) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
reason: `${input.label} must be within repo root (${rootResolved}): ${input.candidatePath}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const rel = relative(rootResolved, candidateResolved);
|
|
75
|
+
if (!rel || rel === ".") {
|
|
76
|
+
return { ok: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const segmentCheck = await checkSegmentsWithinRoot(rootResolved, rootReal, rel, input.label);
|
|
80
|
+
if (!segmentCheck.ok) {
|
|
81
|
+
return segmentCheck;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const candidateReal = await realpath(candidateResolved);
|
|
86
|
+
if (!isSubpath(candidateReal, rootReal)) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
reason: `${input.label} must resolve within repo root (${rootReal}): ${input.candidatePath}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (
|
|
94
|
+
error instanceof Error &&
|
|
95
|
+
"code" in error &&
|
|
96
|
+
(error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
97
|
+
)
|
|
98
|
+
return { ok: true };
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { ok: true };
|
|
103
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const CONFIG_PRIMARY = join(".workbox", "config.toml");
|
|
5
|
+
export const CONFIG_SECONDARY = "workbox.toml";
|
|
6
|
+
export const GLOBAL_CONFIG_XDG = join("workbox", "config.toml");
|
|
7
|
+
export const GLOBAL_CONFIG_FALLBACK = join(".workbox", "config.toml");
|
|
8
|
+
|
|
9
|
+
export const getGlobalConfigPath = (
|
|
10
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
11
|
+
homeDir = homedir()
|
|
12
|
+
): string =>
|
|
13
|
+
env.XDG_CONFIG_HOME
|
|
14
|
+
? join(env.XDG_CONFIG_HOME, GLOBAL_CONFIG_XDG)
|
|
15
|
+
: join(homeDir, GLOBAL_CONFIG_FALLBACK);
|
|
16
|
+
|
|
17
|
+
export const getProjectConfigCandidatePaths = (repoRoot: string): string[] => [
|
|
18
|
+
join(repoRoot, CONFIG_PRIMARY),
|
|
19
|
+
join(repoRoot, CONFIG_SECONDARY),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const resolveWorktreesDir = (worktreesDir: string, repoRoot: string): string =>
|
|
23
|
+
resolve(repoRoot, worktreesDir);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
type RunMode = "inherit" | "capture";
|
|
2
|
+
|
|
3
|
+
type RunResult = {
|
|
4
|
+
exitCode: number;
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const readStream = async (
|
|
10
|
+
stream: ReadableStream<Uint8Array> | null | undefined
|
|
11
|
+
): Promise<string> => {
|
|
12
|
+
if (!stream) {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
return new Response(stream).text();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const runCommand = async (input: {
|
|
19
|
+
cmd: string[];
|
|
20
|
+
cwd: string;
|
|
21
|
+
mode: RunMode;
|
|
22
|
+
env?: Record<string, string>;
|
|
23
|
+
}): Promise<RunResult> => {
|
|
24
|
+
const proc = Bun.spawn({
|
|
25
|
+
cmd: input.cmd,
|
|
26
|
+
cwd: input.cwd,
|
|
27
|
+
env: input.env ? { ...process.env, ...input.env } : process.env,
|
|
28
|
+
stdout: input.mode === "inherit" ? "inherit" : "pipe",
|
|
29
|
+
stderr: input.mode === "inherit" ? "inherit" : "pipe",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
33
|
+
input.mode === "capture" ? readStream(proc.stdout) : Promise.resolve(""),
|
|
34
|
+
input.mode === "capture" ? readStream(proc.stderr) : Promise.resolve(""),
|
|
35
|
+
proc.exited,
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const runShellCommand = async (input: {
|
|
42
|
+
command: string;
|
|
43
|
+
cwd: string;
|
|
44
|
+
mode: RunMode;
|
|
45
|
+
env?: Record<string, string>;
|
|
46
|
+
}): Promise<RunResult> => {
|
|
47
|
+
const isWindows = process.platform === "win32";
|
|
48
|
+
const cmd = isWindows
|
|
49
|
+
? ["cmd.exe", "/d", "/s", "/c", input.command]
|
|
50
|
+
: ["sh", "-c", input.command];
|
|
51
|
+
return runCommand({ cmd, cwd: input.cwd, mode: input.mode, env: input.env });
|
|
52
|
+
};
|
package/src/core/repo.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { dirname, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { CliError } from "../ui/errors";
|
|
4
|
+
import { runCommand } from "./process";
|
|
5
|
+
|
|
6
|
+
const runGit = async (args: string[], cwd: string): Promise<string> => {
|
|
7
|
+
const { stdout, stderr, exitCode } = await runCommand({
|
|
8
|
+
cmd: ["git", ...args],
|
|
9
|
+
cwd,
|
|
10
|
+
mode: "capture",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (exitCode !== 0) {
|
|
14
|
+
const message = stderr.trim() || stdout.trim() || "Unknown git error.";
|
|
15
|
+
throw new CliError(`Git command failed (git ${args.join(" ")}): ${message}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return stdout.trim();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const getRepoInfo = async (
|
|
22
|
+
cwd: string
|
|
23
|
+
): Promise<{ repoRoot: string; worktreeRoot: string; gitCommonDir: string }> => {
|
|
24
|
+
const worktreeRoot = await runGit(["rev-parse", "--show-toplevel"], cwd);
|
|
25
|
+
if (!worktreeRoot) {
|
|
26
|
+
throw new CliError("Unable to resolve Git worktree root.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const commonDirRaw = await runGit(["rev-parse", "--git-common-dir"], cwd);
|
|
30
|
+
if (!commonDirRaw) {
|
|
31
|
+
throw new CliError("Unable to resolve Git common directory.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// `--git-common-dir` is typically relative to the current working directory,
|
|
35
|
+
// not the worktree root. Resolving it relative to `cwd` ensures this works
|
|
36
|
+
// from nested subdirectories.
|
|
37
|
+
const gitCommonDir = resolve(cwd, commonDirRaw);
|
|
38
|
+
const repoRoot = dirname(gitCommonDir);
|
|
39
|
+
if (!repoRoot) {
|
|
40
|
+
throw new CliError("Unable to resolve Git repository root.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { repoRoot, worktreeRoot, gitCommonDir };
|
|
44
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { copyFile, mkdir, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { checkPathWithinRoot } from "../core/path";
|
|
5
|
+
import { runShellCommand } from "../core/process";
|
|
6
|
+
import { CliError } from "../ui/errors";
|
|
7
|
+
|
|
8
|
+
type ProvisionCopy = {
|
|
9
|
+
from: string;
|
|
10
|
+
to: string;
|
|
11
|
+
required: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ProvisionStep = {
|
|
15
|
+
name: string;
|
|
16
|
+
run: string;
|
|
17
|
+
cwd?: string;
|
|
18
|
+
env?: Record<string, string>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ProvisionCopyResult = {
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
required: boolean;
|
|
25
|
+
source: string;
|
|
26
|
+
destination: string;
|
|
27
|
+
status: "copied" | "skipped" | "failed";
|
|
28
|
+
reason?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type ProvisionStepResult = {
|
|
32
|
+
name: string;
|
|
33
|
+
command: string;
|
|
34
|
+
cwd: string;
|
|
35
|
+
exitCode: number;
|
|
36
|
+
stdout?: string;
|
|
37
|
+
stderr?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type ProvisionResult = {
|
|
41
|
+
status: "ok" | "failed";
|
|
42
|
+
message: string;
|
|
43
|
+
copies: ProvisionCopyResult[];
|
|
44
|
+
steps: ProvisionStepResult[];
|
|
45
|
+
exitCode: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type OutputMode = "inherit" | "capture";
|
|
49
|
+
|
|
50
|
+
const isMissingPathError = (error: unknown): boolean =>
|
|
51
|
+
error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
52
|
+
|
|
53
|
+
const getErrorMessage = (error: unknown): string =>
|
|
54
|
+
error instanceof Error ? error.message : String(error);
|
|
55
|
+
|
|
56
|
+
const resolveWithinRoot = async (rootDir: string, path: string, label: string): Promise<string> => {
|
|
57
|
+
const resolved = resolve(rootDir, path);
|
|
58
|
+
const within = await checkPathWithinRoot({
|
|
59
|
+
rootDir,
|
|
60
|
+
candidatePath: resolved,
|
|
61
|
+
label,
|
|
62
|
+
});
|
|
63
|
+
if (!within.ok) {
|
|
64
|
+
throw new CliError(within.reason, { exitCode: 2 });
|
|
65
|
+
}
|
|
66
|
+
return resolved;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const buildFailedResult = (
|
|
70
|
+
message: string,
|
|
71
|
+
copies: ProvisionCopyResult[],
|
|
72
|
+
steps: ProvisionStepResult[],
|
|
73
|
+
exitCode: number
|
|
74
|
+
): ProvisionResult => ({
|
|
75
|
+
status: "failed",
|
|
76
|
+
message,
|
|
77
|
+
copies,
|
|
78
|
+
steps,
|
|
79
|
+
exitCode,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const runProvision = async (
|
|
83
|
+
provision: { copy: ProvisionCopy[]; steps: ProvisionStep[] },
|
|
84
|
+
options: {
|
|
85
|
+
sourceRoot: string;
|
|
86
|
+
targetRoot: string;
|
|
87
|
+
worktreeName: string;
|
|
88
|
+
mode: OutputMode;
|
|
89
|
+
}
|
|
90
|
+
): Promise<ProvisionResult> => {
|
|
91
|
+
const copies: ProvisionCopyResult[] = [];
|
|
92
|
+
const steps: ProvisionStepResult[] = [];
|
|
93
|
+
|
|
94
|
+
if (provision.copy.length === 0 && provision.steps.length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
status: "ok",
|
|
97
|
+
message: "no provision actions configured.",
|
|
98
|
+
copies,
|
|
99
|
+
steps,
|
|
100
|
+
exitCode: 0,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const item of provision.copy) {
|
|
105
|
+
const source = await resolveWithinRoot(options.sourceRoot, item.from, "provision copy source");
|
|
106
|
+
const destination = await resolveWithinRoot(
|
|
107
|
+
options.targetRoot,
|
|
108
|
+
item.to,
|
|
109
|
+
"provision copy destination"
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
let sourceStat: Awaited<ReturnType<typeof stat>>;
|
|
113
|
+
try {
|
|
114
|
+
sourceStat = await stat(source);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (!isMissingPathError(error)) {
|
|
117
|
+
const reason = getErrorMessage(error);
|
|
118
|
+
copies.push({ ...item, source, destination, status: "failed", reason });
|
|
119
|
+
return buildFailedResult(
|
|
120
|
+
`provision copy "${item.from}" failed: ${reason}`,
|
|
121
|
+
copies,
|
|
122
|
+
steps,
|
|
123
|
+
1
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (!item.required) {
|
|
127
|
+
copies.push({ ...item, source, destination, status: "skipped", reason: "missing source" });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const reason = "missing required source";
|
|
131
|
+
copies.push({ ...item, source, destination, status: "failed", reason });
|
|
132
|
+
return buildFailedResult(
|
|
133
|
+
`provision copy "${item.from}" failed: ${reason}.`,
|
|
134
|
+
copies,
|
|
135
|
+
steps,
|
|
136
|
+
1
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (sourceStat.isDirectory()) {
|
|
141
|
+
const reason = "source is a directory";
|
|
142
|
+
copies.push({ ...item, source, destination, status: "failed", reason });
|
|
143
|
+
return buildFailedResult(
|
|
144
|
+
`provision copy "${item.from}" failed: ${reason}.`,
|
|
145
|
+
copies,
|
|
146
|
+
steps,
|
|
147
|
+
1
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
153
|
+
await copyFile(source, destination);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const reason = getErrorMessage(error);
|
|
156
|
+
copies.push({ ...item, source, destination, status: "failed", reason });
|
|
157
|
+
return buildFailedResult(`provision copy "${item.from}" failed: ${reason}`, copies, steps, 1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
copies.push({ ...item, source, destination, status: "copied" });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const step of provision.steps) {
|
|
164
|
+
const cwd = await resolveWithinRoot(options.targetRoot, step.cwd ?? ".", "provision step cwd");
|
|
165
|
+
const result = await runShellCommand({
|
|
166
|
+
command: step.run,
|
|
167
|
+
cwd,
|
|
168
|
+
mode: options.mode,
|
|
169
|
+
env: {
|
|
170
|
+
WORKBOX_SOURCE: options.sourceRoot,
|
|
171
|
+
WORKBOX_WORKTREE: options.targetRoot,
|
|
172
|
+
WORKBOX_NAME: options.worktreeName,
|
|
173
|
+
...step.env,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
steps.push({
|
|
178
|
+
name: step.name,
|
|
179
|
+
command: step.run,
|
|
180
|
+
cwd,
|
|
181
|
+
exitCode: result.exitCode,
|
|
182
|
+
...(options.mode === "capture"
|
|
183
|
+
? { stdout: result.stdout.trim(), stderr: result.stderr.trim() }
|
|
184
|
+
: {}),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (result.exitCode !== 0) {
|
|
188
|
+
return buildFailedResult(
|
|
189
|
+
`provision step "${step.name}" failed (exit ${result.exitCode}).`,
|
|
190
|
+
copies,
|
|
191
|
+
steps,
|
|
192
|
+
result.exitCode
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
status: "ok",
|
|
199
|
+
message: "provision completed.",
|
|
200
|
+
copies,
|
|
201
|
+
steps,
|
|
202
|
+
exitCode: 0,
|
|
203
|
+
};
|
|
204
|
+
};
|
package/src/ui/errors.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class CliError extends Error {
|
|
2
|
+
readonly exitCode: number;
|
|
3
|
+
|
|
4
|
+
constructor(message: string, options?: { exitCode?: number; cause?: unknown }) {
|
|
5
|
+
super(message, options);
|
|
6
|
+
this.name = "CliError";
|
|
7
|
+
this.exitCode = options?.exitCode ?? 1;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class UsageError extends CliError {
|
|
12
|
+
constructor(message: string) {
|
|
13
|
+
super(message, { exitCode: 2 });
|
|
14
|
+
this.name = "UsageError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ConfigError extends CliError {
|
|
19
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
20
|
+
super(message, { exitCode: 1, cause: options?.cause });
|
|
21
|
+
this.name = "ConfigError";
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/ui/log.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type OutputMode = "text" | "json";
|
|
2
|
+
|
|
3
|
+
type CliPayload = {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
command?: string;
|
|
6
|
+
message?: string;
|
|
7
|
+
data?: unknown;
|
|
8
|
+
help?: string;
|
|
9
|
+
errors?: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const formatOutput = (payload: CliPayload, mode: OutputMode): string => {
|
|
13
|
+
if (mode === "json") {
|
|
14
|
+
return JSON.stringify(payload, null, 2);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const lines: string[] = [];
|
|
18
|
+
if (payload.message) {
|
|
19
|
+
lines.push(payload.message);
|
|
20
|
+
}
|
|
21
|
+
if (payload.help) {
|
|
22
|
+
lines.push(payload.help);
|
|
23
|
+
}
|
|
24
|
+
if (payload.errors && payload.errors.length > 0) {
|
|
25
|
+
lines.push("Errors:");
|
|
26
|
+
for (const error of payload.errors) {
|
|
27
|
+
lines.push(`- ${error}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
};
|