@ultimat3/cli 1.1.0 → 2.0.0
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/CLAUDE.md +724 -0
- package/README.md +41 -9
- package/package.json +25 -23
- package/src/api-routes.ts +16 -0
- package/src/app-auth.ts +32 -0
- package/src/app-entities.ts +18 -0
- package/src/app-env.ts +103 -0
- package/src/app-load.ts +20 -3
- package/src/bin.ts +4 -3
- package/src/budgets.ts +114 -9
- package/src/cmd-build.ts +69 -21
- package/src/cmd-db-branch.ts +215 -0
- package/src/cmd-db.ts +332 -155
- package/src/cmd-deploy.ts +59 -6
- package/src/cmd-dev.ts +87 -17
- package/src/cmd-docs.ts +167 -0
- package/src/cmd-doctor.ts +64 -9
- package/src/cmd-env.ts +95 -0
- package/src/cmd-errors.ts +33 -13
- package/src/cmd-fix.ts +5 -1
- package/src/cmd-generate.ts +146 -111
- package/src/cmd-help.ts +16 -5
- package/src/cmd-i18n.ts +2 -0
- package/src/cmd-jobs.ts +47 -33
- package/src/cmd-mcp.ts +11 -2
- package/src/cmd-new.ts +13 -7
- package/src/cmd-planned.ts +55 -10
- package/src/cmd-policy.ts +1 -0
- package/src/cmd-registries.ts +3 -0
- package/src/cmd-secrets.ts +368 -0
- package/src/cmd-tasks.ts +1 -0
- package/src/cmd-test.ts +17 -23
- package/src/cmd-verify.ts +177 -23
- package/src/db-backfill.ts +401 -0
- package/src/db-branch.ts +251 -0
- package/src/db-destructive.ts +29 -0
- package/src/db-finding.ts +28 -0
- package/src/db-generate.ts +112 -0
- package/src/db-snapshot.ts +24 -0
- package/src/dev-assets.ts +86 -20
- package/src/dev-cache.ts +122 -0
- package/src/dev-dashboard.ts +19 -4
- package/src/dev-hooks.ts +27 -2
- package/src/dev-n-plus-one.ts +191 -0
- package/src/dev-queue.ts +105 -19
- package/src/dev-render.ts +158 -26
- package/src/dev-roles-fixture.ts +67 -0
- package/src/dev-roles.ts +186 -78
- package/src/dev-runtime.ts +117 -40
- package/src/dev-services.ts +15 -0
- package/src/dev-storage.ts +245 -0
- package/src/dev-sync.ts +107 -0
- package/src/dev-traces.ts +11 -3
- package/src/dispatch.ts +4 -2
- package/src/document-styles.ts +54 -0
- package/src/drift.ts +37 -9
- package/src/error-catalog.ts +7 -18
- package/src/error-codes.ts +186 -0
- package/src/error-contract.ts +29 -7
- package/src/error-fixes.ts +114 -0
- package/src/errors.ts +205 -140
- package/src/fix-command.ts +268 -0
- package/src/flag-number.ts +56 -0
- package/src/framework-scope.ts +49 -0
- package/src/generate-kinds.ts +97 -0
- package/src/guards.ts +186 -0
- package/src/index.ts +87 -14
- package/src/island-bundle.ts +166 -0
- package/src/island-routes.ts +50 -0
- package/src/jobs-driver.ts +33 -0
- package/src/jobs-json.ts +24 -0
- package/src/jobs-report.ts +17 -4
- package/src/mcp-db-target.ts +52 -27
- package/src/mcp-errors.ts +120 -19
- package/src/mcp-host.ts +44 -25
- package/src/messages.ts +81 -2
- package/src/metrics-endpoint.ts +73 -0
- package/src/migrations.ts +37 -4
- package/src/otlp-export.ts +64 -0
- package/src/output.ts +46 -16
- package/src/parse.ts +41 -3
- package/src/policy-facts.ts +38 -6
- package/src/policy-fixture.ts +14 -7
- package/src/prerender.ts +111 -2
- package/src/registry.ts +21 -3
- package/src/runtime-overrides.ts +66 -0
- package/src/safe-url-label.ts +24 -0
- package/src/scaffold-fixture.ts +10 -0
- package/src/scaffold-typecheck.ts +16 -38
- package/src/serve.ts +202 -18
- package/src/source-files.ts +4 -0
- package/src/statement-loop.ts +74 -0
- package/src/style-csp.ts +18 -0
- package/src/sync-authenticator.ts +59 -0
- package/src/templates/action.ts +15 -30
- package/src/templates/admin-page.ts +103 -0
- package/src/templates/admin.ts +11 -7
- package/src/templates/backfill.ts +212 -0
- package/src/templates/entity.ts +72 -31
- package/src/templates/guard.ts +143 -0
- package/src/templates/index.ts +12 -1
- package/src/templates/island.ts +67 -0
- package/src/templates/job.ts +53 -13
- package/src/templates/naming.ts +17 -1
- package/src/templates/policy.ts +35 -28
- package/src/templates/query.ts +24 -5
- package/src/templates/resource.ts +19 -11
- package/src/templates/route.ts +90 -15
- package/src/templates/scaffold-app.ts +142 -45
- package/src/templates/scaffold-claude-agents.ts +149 -0
- package/src/templates/scaffold-claude-commands.ts +221 -0
- package/src/templates/scaffold-claude.ts +134 -0
- package/src/templates/scaffold-container.ts +46 -2
- package/src/templates/scaffold-db-package.ts +91 -0
- package/src/templates/scaffold-docs.ts +24 -5
- package/src/templates/scaffold-domain-package.ts +90 -0
- package/src/templates/scaffold-env.ts +87 -0
- package/src/templates/scaffold-i18n.ts +4 -1
- package/src/templates/scaffold-mcp-package.ts +49 -0
- package/src/templates/scaffold-package-shape.ts +25 -4
- package/src/templates/scaffold-repo.ts +116 -257
- package/src/templates/scaffold-roles.ts +68 -0
- package/src/templates/scaffold-ui-package.ts +56 -0
- package/src/templates/slice-foundation.ts +88 -0
- package/src/templates/wrap.ts +95 -0
- package/src/test-counts.ts +35 -0
- package/src/test-select.ts +30 -15
- package/src/test-shards.ts +21 -3
- package/src/test-workers.ts +47 -0
- package/src/ts-scan.ts +271 -13
- package/src/tsconfig-references.ts +78 -0
- package/src/verify-floor.ts +133 -0
- package/src/verify-step.ts +19 -0
- package/src/verify-test-run.ts +72 -0
- package/src/verify-tests.ts +160 -71
- package/src/version-loader.ts +20 -3
- package/src/workspace-checks.ts +87 -16
- package/src/write-line.ts +34 -0
package/src/cmd-build.ts
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { frameworkVersion, VERSION_DEFINE } from '@ultimat3/core';
|
|
6
7
|
import { requireAppRoot } from './app-root';
|
|
7
8
|
import { runVerify } from './cmd-verify';
|
|
8
9
|
import type { CliCommand, CommandContext } from './command';
|
|
9
10
|
import { BuildEntryMissingError, UnknownCommandError } from './errors';
|
|
11
|
+
import type { ExecResult } from './exec';
|
|
10
12
|
import { execOutput } from './exec';
|
|
11
13
|
import { msg } from './messages';
|
|
12
|
-
import type { CommandResult
|
|
14
|
+
import type { CommandResult } from './output';
|
|
13
15
|
import { flagString } from './parse';
|
|
14
16
|
|
|
15
17
|
export const BUILD_TARGETS = ['docker', 'binary', 'static'] as const;
|
|
@@ -51,12 +53,20 @@ export function dockerArgs(root: string, tag: string): readonly string[] {
|
|
|
51
53
|
return ['docker', 'build', '-f', join(root, BUILD_ENTRY.docker), '-t', tag, root];
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The define is not optional. A single-file executable carries no `package.json`, so
|
|
58
|
+
* `frameworkVersion()` has nothing to read and throws — which is exactly how this target came to
|
|
59
|
+
* compile an artifact that could never boot. The value is this CLI's own `@ultimat3/core`, which is
|
|
60
|
+
* the app's too: the packages release in lockstep and `x new` pins them together.
|
|
61
|
+
*/
|
|
54
62
|
export function binaryArgs(root: string, out: string): readonly string[] {
|
|
55
63
|
return [
|
|
56
64
|
'bun',
|
|
57
65
|
'build',
|
|
58
66
|
'--compile',
|
|
59
67
|
'--minify',
|
|
68
|
+
'--define',
|
|
69
|
+
`${VERSION_DEFINE}=${JSON.stringify(frameworkVersion())}`,
|
|
60
70
|
join(root, BUILD_ENTRY.binary),
|
|
61
71
|
'--outfile',
|
|
62
72
|
out,
|
|
@@ -76,6 +86,57 @@ export function argsFor(
|
|
|
76
86
|
return staticArgs(paths.root, paths.out);
|
|
77
87
|
}
|
|
78
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The static gate refused, so nothing was built. Reported under `build`, not `verify`: `command`
|
|
91
|
+
* is the field an agent keys `--json` off, and answering `"verify"` sent it to re-run a gate it
|
|
92
|
+
* never asked for while hiding that the build had not started. The steps and the summary are the
|
|
93
|
+
* gate's own — they are what says which check to fix.
|
|
94
|
+
*/
|
|
95
|
+
export function preflightResult(verify: CommandResult): CommandResult {
|
|
96
|
+
return { ...verify, command: 'build' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The build's result from the builder's, kept pure so the two things a reader acts on — the
|
|
101
|
+
* summary line and the `--json` payload — are testable without spawning `docker`.
|
|
102
|
+
*
|
|
103
|
+
* `summary` used to be `msg('cli.build.done')` whatever the exit code, so a failed build printed
|
|
104
|
+
* `✗ built docker`; and the builder's own logs went only into `lines`, which is declared human-only
|
|
105
|
+
* and which `renderJson` drops — so CI, which runs `--json`, got the exit code and nothing to act
|
|
106
|
+
* on. The output now rides in `data` and `lines` renders that same string.
|
|
107
|
+
*/
|
|
108
|
+
export function buildResult(input: {
|
|
109
|
+
readonly target: BuildTarget;
|
|
110
|
+
readonly artifact: string;
|
|
111
|
+
readonly command: readonly string[];
|
|
112
|
+
readonly result: ExecResult;
|
|
113
|
+
}): CommandResult {
|
|
114
|
+
const { result, target } = input;
|
|
115
|
+
const output = result.ok ? '' : execOutput(result);
|
|
116
|
+
return {
|
|
117
|
+
ok: result.ok,
|
|
118
|
+
command: 'build',
|
|
119
|
+
summary: msg(result.ok ? 'cli.build.done' : 'cli.build.failed', { target }),
|
|
120
|
+
findings: result.ok
|
|
121
|
+
? []
|
|
122
|
+
: [
|
|
123
|
+
{
|
|
124
|
+
code: 'X_BUILD_FAILED',
|
|
125
|
+
cause: `${input.command.join(' ')} exited ${result.code}`,
|
|
126
|
+
fix: target === 'docker' ? 'x doctor --json && docker info' : 'x verify --json',
|
|
127
|
+
docs: 'https://ultimate.dev/errors/X_BUILD_FAILED',
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
data: {
|
|
131
|
+
target,
|
|
132
|
+
artifact: input.artifact,
|
|
133
|
+
durationMs: result.durationMs,
|
|
134
|
+
...(result.ok ? {} : { output }),
|
|
135
|
+
},
|
|
136
|
+
lines: result.ok ? [] : output.split('\n'),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
79
140
|
export const buildCommand: CliCommand = {
|
|
80
141
|
spec: {
|
|
81
142
|
name: 'build',
|
|
@@ -103,31 +164,18 @@ export const buildCommand: CliCommand = {
|
|
|
103
164
|
);
|
|
104
165
|
const verifyResult = await runVerify(verifySteps, { root, runner: ctx.runner });
|
|
105
166
|
if (!verifyResult.ok) {
|
|
106
|
-
return verifyResult;
|
|
167
|
+
return preflightResult(verifyResult);
|
|
107
168
|
}
|
|
108
169
|
|
|
109
170
|
const out =
|
|
110
171
|
flagString(ctx.args, 'out') ?? join(root, '.x', target === 'static' ? 'static' : 'app');
|
|
111
172
|
const tag = flagString(ctx.args, 'tag') ?? 'ultimate-app:dev';
|
|
112
173
|
const command = argsFor(target, { root, tag, out });
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
?
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
cause: `${command.join(' ')} exited ${result.code}`,
|
|
120
|
-
fix: target === 'docker' ? 'x doctor --json && docker info' : 'x verify --json',
|
|
121
|
-
docs: 'https://ultimate.dev/errors/X_BUILD_FAILED',
|
|
122
|
-
},
|
|
123
|
-
];
|
|
124
|
-
return {
|
|
125
|
-
ok: result.ok,
|
|
126
|
-
command: 'build',
|
|
127
|
-
summary: msg('cli.build.done', { target }),
|
|
128
|
-
findings,
|
|
129
|
-
data: { target, artifact: target === 'docker' ? tag : out, durationMs: result.durationMs },
|
|
130
|
-
lines: result.ok ? [] : execOutput(result).split('\n'),
|
|
131
|
-
};
|
|
174
|
+
return buildResult({
|
|
175
|
+
target,
|
|
176
|
+
artifact: target === 'docker' ? tag : out,
|
|
177
|
+
command,
|
|
178
|
+
result: await ctx.runner(command, { cwd: root }),
|
|
179
|
+
});
|
|
132
180
|
},
|
|
133
181
|
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// `x db branch ls|create|drop` — the wiring alone: which verb, which database, which refusal.
|
|
2
|
+
// A VERB is required and comes from a closed set, so a branch name can never be read as one:
|
|
3
|
+
// `x db branch ls` used to clone a database called `ls`, because the argument was the name.
|
|
4
|
+
// The facts (what a branch is, per mode) are `db-branch.ts`; the client lifetime is here.
|
|
5
|
+
|
|
6
|
+
import { createPostgresClient, type DbClient } from '@ultimat3/db';
|
|
7
|
+
import type { CommandContext } from './command';
|
|
8
|
+
import type { BranchRow } from './db-branch';
|
|
9
|
+
import {
|
|
10
|
+
BRANCH_SUBCOMMANDS,
|
|
11
|
+
branchDatabaseName,
|
|
12
|
+
createExternalBranch,
|
|
13
|
+
createPgliteBranch,
|
|
14
|
+
dropExternalBranch,
|
|
15
|
+
dropPgliteBranch,
|
|
16
|
+
isBranchName,
|
|
17
|
+
isBranchSubcommand,
|
|
18
|
+
listExternalBranches,
|
|
19
|
+
listPgliteBranches,
|
|
20
|
+
pgliteBranchLocation,
|
|
21
|
+
previewUrl,
|
|
22
|
+
} from './db-branch';
|
|
23
|
+
import { stepFinding } from './db-finding';
|
|
24
|
+
import type { DevServices } from './dev-services';
|
|
25
|
+
import { resolveServices } from './dev-services';
|
|
26
|
+
import { MissingPositionalError, UnknownCommandError } from './errors';
|
|
27
|
+
import { msg } from './messages';
|
|
28
|
+
import type { CommandResult, Finding } from './output';
|
|
29
|
+
import { flagString, nearest } from './parse';
|
|
30
|
+
import { renderTable } from './table';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Always runnable, always the next thing a caller needs: what branches there are. Spelled twice
|
|
34
|
+
* because `UnknownCommandError` prefixes its `suggestion` with `x ` and `MissingPositionalError`
|
|
35
|
+
* takes a whole invocation — one of them handed back `x x db branch ls --json`.
|
|
36
|
+
*/
|
|
37
|
+
const LIST_ARGV = 'db branch ls --json';
|
|
38
|
+
const LIST_FIX = `x ${LIST_ARGV}`;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A near miss on a three-verb set is a typo; anything else is the bare-name form this replaced —
|
|
42
|
+
* `x db branch feat-new-billing` used to CREATE, so the refusal hands the caller's own name back
|
|
43
|
+
* inside the command that still does it. Every answer here is a complete, runnable invocation,
|
|
44
|
+
* `x` excluded — the error class adds it.
|
|
45
|
+
*/
|
|
46
|
+
function branchRetry(word: string, name: string | undefined): string {
|
|
47
|
+
const near = nearest(word, [...BRANCH_SUBCOMMANDS]);
|
|
48
|
+
if (near !== undefined) return name === undefined ? LIST_ARGV : `db branch ${near} ${name}`;
|
|
49
|
+
return isBranchName(word) ? `db branch create ${word}` : LIST_ARGV;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `x db branch` against an external Postgres, on ONE connection.
|
|
54
|
+
*
|
|
55
|
+
* `role: 'migrate'` is the profile, not a decoration: it is `max: 1` with no statement timeout, and
|
|
56
|
+
* both halves are load-bearing. `CREATE DATABASE ... TEMPLATE` is refused while any OTHER session
|
|
57
|
+
* is connected to the template, so a pool that spread three statements over three connections
|
|
58
|
+
* would leave two idle sessions holding the source open against itself; and cloning a real
|
|
59
|
+
* database routinely outlives the 10s a `web` profile allows.
|
|
60
|
+
*/
|
|
61
|
+
async function withBranchClient<T>(url: string, fn: (client: DbClient) => Promise<T>): Promise<T> {
|
|
62
|
+
const client = createPostgresClient({ url, role: 'migrate', applicationName: 'x-db-branch' });
|
|
63
|
+
try {
|
|
64
|
+
return await fn(client);
|
|
65
|
+
} finally {
|
|
66
|
+
// Or the CLI exits holding a connection, and the next command waits for it to time out.
|
|
67
|
+
await client.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const branchesOf = (services: DevServices): Promise<readonly BranchRow[]> =>
|
|
72
|
+
services.db.mode === 'embedded'
|
|
73
|
+
? listPgliteBranches(services.db.url)
|
|
74
|
+
: withBranchClient(services.db.url, listExternalBranches);
|
|
75
|
+
|
|
76
|
+
const failure = (summary: string, finding: Finding): CommandResult => ({
|
|
77
|
+
ok: false,
|
|
78
|
+
command: 'db',
|
|
79
|
+
summary,
|
|
80
|
+
findings: [finding],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export async function runBranchCommand(ctx: CommandContext, root: string): Promise<CommandResult> {
|
|
84
|
+
const verb = ctx.args.positionals[0];
|
|
85
|
+
if (verb === undefined) {
|
|
86
|
+
throw new MissingPositionalError({
|
|
87
|
+
command: 'db branch',
|
|
88
|
+
positional: BRANCH_SUBCOMMANDS.join('|'),
|
|
89
|
+
example: LIST_FIX,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const name = ctx.args.positionals[1] ?? flagString(ctx.args, 'name');
|
|
93
|
+
if (!isBranchSubcommand(verb)) {
|
|
94
|
+
throw new UnknownCommandError({
|
|
95
|
+
path: `db branch ${verb}`,
|
|
96
|
+
known: BRANCH_SUBCOMMANDS,
|
|
97
|
+
suggestion: branchRetry(verb, name),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const services = resolveServices(root, ctx.env);
|
|
101
|
+
if (verb === 'ls') return runList(services);
|
|
102
|
+
if (name === undefined) {
|
|
103
|
+
throw new MissingPositionalError({
|
|
104
|
+
command: `db branch ${verb}`,
|
|
105
|
+
positional: 'name',
|
|
106
|
+
example: `x db branch ${verb} feature-x`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return verb === 'create' ? runCreate(ctx, services, name) : runDrop(services, name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const row = (branch: BranchRow): readonly string[] => [
|
|
113
|
+
branch.name,
|
|
114
|
+
branch.location,
|
|
115
|
+
branch.createdAt ?? msg('cli.db.branch.unknown'),
|
|
116
|
+
branch.sizeBytes === null ? msg('cli.db.branch.unknown') : String(branch.sizeBytes),
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
async function runList(services: DevServices): Promise<CommandResult> {
|
|
120
|
+
let branches: readonly BranchRow[];
|
|
121
|
+
try {
|
|
122
|
+
branches = await branchesOf(services);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return failure(msg('cli.db.branch.failed'), stepFinding(error, 'X_DB_BRANCH_FAILED'));
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
command: 'db',
|
|
129
|
+
summary:
|
|
130
|
+
branches.length === 0
|
|
131
|
+
? msg('cli.db.branch.none')
|
|
132
|
+
: msg('cli.db.branch.listed', { count: branches.length }),
|
|
133
|
+
lines:
|
|
134
|
+
branches.length === 0
|
|
135
|
+
? []
|
|
136
|
+
: renderTable(['name', 'location', 'created-at', 'size-bytes'], branches.map(row)).map(
|
|
137
|
+
(line) => ` ${line}`,
|
|
138
|
+
),
|
|
139
|
+
data: branches.map((branch) => ({ ...branch })),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function runCreate(
|
|
144
|
+
ctx: CommandContext,
|
|
145
|
+
services: DevServices,
|
|
146
|
+
name: string,
|
|
147
|
+
): Promise<CommandResult> {
|
|
148
|
+
const port = Number.parseInt(ctx.env['PORT'] ?? '3000', 10);
|
|
149
|
+
let branch: BranchRow;
|
|
150
|
+
try {
|
|
151
|
+
branch =
|
|
152
|
+
services.db.mode === 'embedded'
|
|
153
|
+
? await createPgliteBranch(services.db.url, name)
|
|
154
|
+
: await withBranchClient(services.db.url, (client) => createExternalBranch(client, name));
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return failure(msg('cli.db.branch.failed'), stepFinding(error, 'X_DB_BRANCH_FAILED'));
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
command: 'db',
|
|
161
|
+
summary: msg('cli.db.branch.ready', { name: branch.name }),
|
|
162
|
+
data: {
|
|
163
|
+
branch: branch.name,
|
|
164
|
+
database: branch.location,
|
|
165
|
+
preview: previewUrl(branch.name, port),
|
|
166
|
+
mode: services.db.mode,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* You may only drop what `ls` shows, and that is the whole guard — stronger than a confirmation
|
|
173
|
+
* flag, because it is the typo that is impossible rather than the keystroke that is tedious. An
|
|
174
|
+
* external branch is a database carrying `createBranch`'s marker comment AND this database's own
|
|
175
|
+
* prefix, so neither the shared database this session is connected to nor another app's clone on
|
|
176
|
+
* the same server is in the set; an embedded one is a `pgdata-<name>` directory, so `pgdata` itself
|
|
177
|
+
* is not either. `@ultimat3/db`'s own `X_BRANCH_EXISTS` fix line is `x db branch drop <name>` with
|
|
178
|
+
* no flag on it, so a flag here would break a shipped instruction.
|
|
179
|
+
*
|
|
180
|
+
* The check is not made here, and that is the point: `false` from either drop means "there was no
|
|
181
|
+
* such branch", decided by the same call that deletes, on the same connection, one statement
|
|
182
|
+
* earlier. A listing taken here and acted on below is two connections and a window wide enough to
|
|
183
|
+
* hold a whole `create` — and the wiring layer is exactly where a guard must not live.
|
|
184
|
+
*/
|
|
185
|
+
async function runDrop(services: DevServices, name: string): Promise<CommandResult> {
|
|
186
|
+
try {
|
|
187
|
+
const dropped =
|
|
188
|
+
services.db.mode === 'embedded'
|
|
189
|
+
? await dropPgliteBranch(services.db.url, name)
|
|
190
|
+
: await withBranchClient(services.db.url, (client) => dropExternalBranch(client, name));
|
|
191
|
+
if (!dropped) return notABranch(services, name);
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
command: 'db',
|
|
195
|
+
summary: msg('cli.db.branch.dropped', { name }),
|
|
196
|
+
data: { branch: name, mode: services.db.mode },
|
|
197
|
+
};
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return failure(msg('cli.db.branch.failed'), stepFinding(error, 'X_DB_BRANCH_FAILED'));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Names what the drop WOULD have touched, so the refusal is checkable rather than assertable. */
|
|
204
|
+
function notABranch(services: DevServices, name: string): CommandResult {
|
|
205
|
+
const target =
|
|
206
|
+
services.db.mode === 'embedded'
|
|
207
|
+
? pgliteBranchLocation(services.db.url, name)
|
|
208
|
+
: branchDatabaseName(services.db.url.split('/').at(-1) ?? 'postgres', name);
|
|
209
|
+
return failure(msg('cli.db.branch.failed'), {
|
|
210
|
+
code: 'X_DB_BRANCH_FAILED',
|
|
211
|
+
cause: `"${name}" is not a branch of this database, so nothing was dropped (it would be ${target})`,
|
|
212
|
+
fix: LIST_FIX,
|
|
213
|
+
docs: 'https://ultimate.dev/errors/X_DB_BRANCH_FAILED',
|
|
214
|
+
});
|
|
215
|
+
}
|