@pithy-sh/cli 0.1.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/LICENSE +21 -0
- package/package.json +72 -0
- package/scripts/templateManifest.ts +49 -0
- package/scripts/tsconfig.json +26 -0
- package/scripts/vendorTemplate.ts +84 -0
- package/scripts/verifyPack.ts +88 -0
- package/src/audit/cliAudit.ts +406 -0
- package/src/bin.ts +111 -0
- package/src/capabilities/add.ts +288 -0
- package/src/capabilities/addBootstrap.ts +275 -0
- package/src/capabilities/catalog.ts +175 -0
- package/src/capabilities/compose.ts +39 -0
- package/src/capabilities/configConstants.ts +74 -0
- package/src/capabilities/configImports.ts +397 -0
- package/src/capabilities/eject.ts +331 -0
- package/src/capabilities/emailProvisioner.ts +346 -0
- package/src/capabilities/entitlementGap.ts +70 -0
- package/src/capabilities/entryExports.ts +162 -0
- package/src/capabilities/flow.ts +550 -0
- package/src/capabilities/hostRegistry.ts +368 -0
- package/src/capabilities/loadFailure.ts +208 -0
- package/src/capabilities/manifests.ts +238 -0
- package/src/capabilities/mediaProvisioner.ts +471 -0
- package/src/capabilities/mintSecrets.ts +306 -0
- package/src/capabilities/paymentsProvisioner.ts +207 -0
- package/src/capabilities/prerequisites.ts +168 -0
- package/src/capabilities/r2Bucket.ts +113 -0
- package/src/capabilities/reconcile.ts +1483 -0
- package/src/capabilities/remove.ts +597 -0
- package/src/capabilities/requiredOptions.ts +92 -0
- package/src/capabilities/rotateSecrets.ts +305 -0
- package/src/capabilities/secrets.ts +178 -0
- package/src/capabilities/secretsDispatcher.ts +29 -0
- package/src/capabilities/secretsProvisioner.ts +389 -0
- package/src/capabilities/storageProvisioner.ts +414 -0
- package/src/capabilities/supportProvisioner.ts +515 -0
- package/src/capabilities/testersLoader.ts +52 -0
- package/src/capabilities/testersProvisioner.ts +236 -0
- package/src/capabilities/turnstileProvisioner.ts +347 -0
- package/src/capabilities/vectorProvisioner.ts +260 -0
- package/src/ci/fileModes.ts +223 -0
- package/src/ci/sourceFiles.ts +200 -0
- package/src/ci/workflowDrivers.ts +524 -0
- package/src/cloudflare/accountAnswer.ts +110 -0
- package/src/cloudflare/config.ts +685 -0
- package/src/cloudflare/storeId.ts +129 -0
- package/src/commands/add.ts +372 -0
- package/src/commands/alias.ts +205 -0
- package/src/commands/dashboard.ts +651 -0
- package/src/commands/deploy.ts +150 -0
- package/src/commands/dev.ts +37 -0
- package/src/commands/doctor.ts +2059 -0
- package/src/commands/email.ts +425 -0
- package/src/commands/env.ts +155 -0
- package/src/commands/feature.ts +359 -0
- package/src/commands/init.ts +538 -0
- package/src/commands/media.ts +303 -0
- package/src/commands/migrate.ts +129 -0
- package/src/commands/payments.ts +336 -0
- package/src/commands/provision.ts +368 -0
- package/src/commands/remove.ts +151 -0
- package/src/commands/secrets.ts +652 -0
- package/src/commands/seed.ts +229 -0
- package/src/commands/storage.ts +309 -0
- package/src/commands/support.ts +331 -0
- package/src/commands/testers.ts +1020 -0
- package/src/commands/token.ts +364 -0
- package/src/commands/turnstile.ts +271 -0
- package/src/commands/ui.ts +222 -0
- package/src/commands/upgrade.ts +517 -0
- package/src/commands/vector.ts +390 -0
- package/src/commands/worker.ts +295 -0
- package/src/dashboard/api.ts +323 -0
- package/src/dashboard/connect.ts +758 -0
- package/src/dashboard/contract.ts +289 -0
- package/src/dashboard/grant.ts +124 -0
- package/src/dashboard/registry.ts +519 -0
- package/src/dashboard/resolveTarget.ts +119 -0
- package/src/dev/delivery.ts +174 -0
- package/src/dev/devLogin.ts +155 -0
- package/src/dev/devLoginTargets.ts +91 -0
- package/src/dev/env.ts +206 -0
- package/src/dev/hostWorkers.ts +290 -0
- package/src/dev/keys.ts +111 -0
- package/src/dev/logging.ts +87 -0
- package/src/dev/openUrl.ts +75 -0
- package/src/dev/orchestrator.ts +1014 -0
- package/src/dev/ports.ts +220 -0
- package/src/dev/readyWatch.ts +142 -0
- package/src/dev/state.ts +90 -0
- package/src/devSecrets/bootstrapVars.ts +265 -0
- package/src/devSecrets/devVars.ts +240 -0
- package/src/devSecrets/edit.ts +256 -0
- package/src/devSecrets/file.ts +277 -0
- package/src/devSecrets/generate.ts +428 -0
- package/src/devSecrets/location.ts +80 -0
- package/src/devSecrets/mode.ts +71 -0
- package/src/devSecrets/records.ts +30 -0
- package/src/devSecrets/report.ts +99 -0
- package/src/devSecrets/seed.ts +344 -0
- package/src/devSecrets/store.ts +262 -0
- package/src/devSecrets/targets.ts +204 -0
- package/src/dispatch.ts +147 -0
- package/src/docs/catalog.ts +246 -0
- package/src/docs/writeCatalog.ts +45 -0
- package/src/doctor/cloudflare.ts +287 -0
- package/src/doctor/devPreferences.ts +155 -0
- package/src/doctor/devSecrets.ts +464 -0
- package/src/doctor/devVars.ts +414 -0
- package/src/doctor/devVarsLocal.ts +138 -0
- package/src/doctor/environments.ts +155 -0
- package/src/doctor/health.ts +354 -0
- package/src/doctor/localDelivery.ts +91 -0
- package/src/doctor/portsRegistry.ts +252 -0
- package/src/doctor/projectName.ts +584 -0
- package/src/doctor/secretBindings.ts +166 -0
- package/src/doctor/settings.ts +274 -0
- package/src/doctor/settingsSources.ts +202 -0
- package/src/doctor/workerName.ts +174 -0
- package/src/doctor/wranglerVars.ts +33 -0
- package/src/feature/bindings.ts +93 -0
- package/src/feature/create.ts +179 -0
- package/src/feature/destroy.ts +160 -0
- package/src/feature/devConfig.ts +201 -0
- package/src/feature/identity.ts +100 -0
- package/src/feature/manifest.ts +132 -0
- package/src/feature/ports.ts +615 -0
- package/src/feature/provision.ts +362 -0
- package/src/feature/sync.ts +148 -0
- package/src/feature/worktree.ts +282 -0
- package/src/help/groups.ts +47 -0
- package/src/help/rootUsage.ts +135 -0
- package/src/main.ts +73 -0
- package/src/migrations/ledger.ts +129 -0
- package/src/migrations/registry.ts +47 -0
- package/src/migrations/run.ts +1066 -0
- package/src/notifier/check.ts +129 -0
- package/src/notifier/installer.ts +48 -0
- package/src/notifier/notify.ts +152 -0
- package/src/notifier/state.ts +248 -0
- package/src/notifier/version.ts +59 -0
- package/src/platform/editor.ts +333 -0
- package/src/platform/rc.ts +118 -0
- package/src/platform/shell.ts +83 -0
- package/src/project/appBindings.ts +184 -0
- package/src/project/appWorkflows.ts +266 -0
- package/src/project/applyDomains.ts +166 -0
- package/src/project/askDomains.ts +220 -0
- package/src/project/atomic.ts +466 -0
- package/src/project/bindingEntries.ts +425 -0
- package/src/project/config.ts +701 -0
- package/src/project/dashboard.ts +118 -0
- package/src/project/deploy.ts +364 -0
- package/src/project/devVars.ts +113 -0
- package/src/project/domainPrompt.ts +191 -0
- package/src/project/domains.ts +386 -0
- package/src/project/envInventory.ts +356 -0
- package/src/project/environment.ts +125 -0
- package/src/project/extensions.ts +69 -0
- package/src/project/jsonc.ts +289 -0
- package/src/project/packageManager.ts +238 -0
- package/src/project/readOptionalFile.ts +342 -0
- package/src/project/rollback.ts +145 -0
- package/src/project/scaffold.ts +1088 -0
- package/src/project/templateFiles.ts +53 -0
- package/src/project/verifyDeploy.ts +230 -0
- package/src/project/versionMetadata.ts +77 -0
- package/src/project/workerAddress.ts +176 -0
- package/src/project/workerCommand.ts +564 -0
- package/src/project/workerIdentity.ts +50 -0
- package/src/project/workerManifest.ts +135 -0
- package/src/project/workerScaffold.ts +289 -0
- package/src/project/workerScope.ts +394 -0
- package/src/project/workers.ts +86 -0
- package/src/project/workflows.ts +281 -0
- package/src/project/wrangler.ts +168 -0
- package/src/provision/confirm.ts +86 -0
- package/src/provision/environment.ts +407 -0
- package/src/provision/featureConfig.ts +98 -0
- package/src/provision/mode.ts +62 -0
- package/src/provision/pendingSecrets.ts +96 -0
- package/src/provision/resources.ts +126 -0
- package/src/provision/secretBindings.ts +149 -0
- package/src/provision/store.ts +33 -0
- package/src/provision/unprovisioned.ts +114 -0
- package/src/provision/wranglerEnv.ts +220 -0
- package/src/rootFlags.ts +48 -0
- package/src/seed/drivers.ts +423 -0
- package/src/seed/media.ts +187 -0
- package/src/seed/plan.ts +137 -0
- package/src/seed/prepare.ts +224 -0
- package/src/seed/registry.ts +25 -0
- package/src/seed/run.ts +793 -0
- package/src/seed/safety.ts +206 -0
- package/src/terminal/logger.ts +42 -0
- package/src/terminal/output.ts +64 -0
- package/src/terminal/style.ts +132 -0
- package/src/test-utils/doctorHarness.ts +190 -0
- package/src/test-utils/migrateHarness.ts +126 -0
- package/src/test-utils/seedHarness.ts +173 -0
- package/src/test-utils/tempRepo.ts +45 -0
- package/src/tokens/config.ts +16 -0
- package/src/tokens/engine.ts +345 -0
- package/src/tokens/mintedTokens.ts +233 -0
- package/src/tokens/sinks.ts +84 -0
- package/src/ui/flow.ts +451 -0
- package/src/ui/react.ts +112 -0
- package/src/ui/routeAllowlist.ts +208 -0
- package/src/ui/scaffold.ts +113 -0
- package/src/ui/screenStyles.ts +127 -0
- package/src/ui/stubs.ts +135 -0
- package/src/ui/templates.ts +52 -0
- package/src/ui/wire.ts +311 -0
- package/src/ui/workerUi.ts +172 -0
- package/templates/starter/.dev.secrets.example.jsonc +43 -0
- package/templates/starter/.dev.vars.example +30 -0
- package/templates/starter/apps/api/package.json +22 -0
- package/templates/starter/apps/api/pithy.config.ts +65 -0
- package/templates/starter/apps/api/pithy.worker.jsonc +11 -0
- package/templates/starter/apps/api/src/bindings.workers.test.ts +18 -0
- package/templates/starter/apps/api/src/cloudflare-test.d.ts +11 -0
- package/templates/starter/apps/api/src/index.ts +8 -0
- package/templates/starter/apps/api/tsconfig.json +26 -0
- package/templates/starter/apps/api/wrangler.jsonc +68 -0
- package/templates/starter/biome.template.jsonc +75 -0
- package/templates/starter/gitignore +37 -0
- package/templates/starter/package.json +28 -0
- package/templates/starter/pithy.config.ts +67 -0
- package/templates/starter/plugins/no-console.grit +25 -0
- package/templates/starter/plugins/no-process-io.grit +25 -0
- package/templates/starter/tsconfig.json +14 -0
- package/templates/starter/tsconfig.tools.json +30 -0
- package/templates/starter/vitest.config.ts +124 -0
- package/templates/starter/vitest.workers.config.ts +26 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import type { BindingSpec } from "@pithy-sh/core/src/capability/bindings";
|
|
7
|
+
import type { Capability } from "@pithy-sh/core/src/capability/capability";
|
|
8
|
+
import { ConflictError, InternalError } from "@pithy-sh/core/src/error/pithyError";
|
|
9
|
+
import type { CliAuditEmit } from "../audit/cliAudit";
|
|
10
|
+
import type { CloudflareAccountSelection } from "../cloudflare/config";
|
|
11
|
+
import { type DatabaseRun, dropCapabilityTables } from "../migrations/run";
|
|
12
|
+
import { uninstallPackage } from "../project/packageManager";
|
|
13
|
+
import { readOptionalFile } from "../project/readOptionalFile";
|
|
14
|
+
import { removeScaffoldPath } from "../project/scaffold";
|
|
15
|
+
import { discoverWorkers } from "../project/workers";
|
|
16
|
+
import { readWranglerConfig, workerEntryPath, writeWranglerConfig } from "../project/wrangler";
|
|
17
|
+
import { capabilityPackageName, isSharedCapabilityPackage } from "./catalog";
|
|
18
|
+
import { exportsName, findNamedImport, isCapabilityImport, withoutBinding } from "./configImports";
|
|
19
|
+
import { EJECT_DIR, ejectImportPath, ejectSpecifierFromDir, isEjected } from "./eject";
|
|
20
|
+
import { durableObjectExports, withoutDurableObjectExports } from "./entryExports";
|
|
21
|
+
|
|
22
|
+
/** The subset of a manifest/capability the binding helpers read — both shapes carry it. */
|
|
23
|
+
type HasBindings = { requiredBindings: readonly BindingSpec[] };
|
|
24
|
+
|
|
25
|
+
/** Escape a capability name for use inside a `RegExp` (names are simple, but be safe). */
|
|
26
|
+
function escapeRegExp(text: string): string {
|
|
27
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Remove a capability's import and registration from a Worker's `pithy.config.ts` — the inverse of `add`'s
|
|
32
|
+
* managed-region wiring. Drops the capability's binding (imported from the package
|
|
33
|
+
* `@pithy-sh/<cap>/src/index` **or** the ejected `./capabilities/<cap>`), then the `<cap>(),`
|
|
34
|
+
* registration — a one-liner, or the whole block form (`<cap>({ … }),`) when the capability carries
|
|
35
|
+
* config options. Idempotent: a config that never had the capability is returned unchanged. Matches
|
|
36
|
+
* only the exact factory call, never a shared-prefix name (`authpro` is left alone when removing
|
|
37
|
+
* `auth`).
|
|
38
|
+
*
|
|
39
|
+
* A block registration whose closing `}),` line can't be found (a hand-edited or reformatted config)
|
|
40
|
+
* **throws** rather than deleting to end-of-file — the function never returns a truncated config, so a
|
|
41
|
+
* malformed input leaves `pithy.config.ts` untouched.
|
|
42
|
+
*/
|
|
43
|
+
export function unwireConfig(source: string, name: string, pkg: string): string {
|
|
44
|
+
const lines = source.split("\n");
|
|
45
|
+
|
|
46
|
+
const registered = new RegExp(`^${escapeRegExp(name)}\\(`);
|
|
47
|
+
const startIndex = lines.findIndex((line) => registered.test(line.trimStart()));
|
|
48
|
+
if (startIndex !== -1) {
|
|
49
|
+
let endIndex = startIndex;
|
|
50
|
+
// A block form opens with `<cap>({` and closes on a `}),` line; a one-liner is `<cap>(),`.
|
|
51
|
+
if (lines[startIndex]?.trimEnd().endsWith("({")) {
|
|
52
|
+
while (endIndex < lines.length && lines[endIndex]?.trim() !== "}),") endIndex += 1;
|
|
53
|
+
if (endIndex >= lines.length) {
|
|
54
|
+
throw new InternalError({
|
|
55
|
+
message: `Couldn't find the end of the ${name}() registration in pithy.config.ts.`,
|
|
56
|
+
action: `Remove the ${name} import and registration by hand.`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
lines.splice(startIndex, endIndex - startIndex + 1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The import is found by binding and taken out only if it comes from the capability itself — the
|
|
64
|
+
// same rule `add` writes by. Matching two exact lines instead left a hand-edited deep import
|
|
65
|
+
// standing while `removeCapability` uninstalled the package under it, which is a config that cannot
|
|
66
|
+
// load: the failure this command exists to undo. An import of the same name from the adopter's own
|
|
67
|
+
// module is not ours and stays.
|
|
68
|
+
// Every one of them, not the first: a config wrecked by the old `add` — a hand-corrected specifier
|
|
69
|
+
// and the broken one put back beside it — carries two, and leaving one is still unloadable once the
|
|
70
|
+
// package goes.
|
|
71
|
+
// Only the capability's own binding goes with it. `import { auth, hashPassword } from …` is one
|
|
72
|
+
// statement holding two answers, and deleting it took the adopter's other name with it.
|
|
73
|
+
const ejectPath = ejectImportPath(name);
|
|
74
|
+
let rest = lines.join("\n");
|
|
75
|
+
for (;;) {
|
|
76
|
+
const found = findNamedImport(rest, name);
|
|
77
|
+
if (!found || !isCapabilityImport(found.specifier, pkg, ejectPath)) return rest;
|
|
78
|
+
const next = withoutBinding(rest, found);
|
|
79
|
+
if (next === rest) return rest; // nothing was taken out; never spin on it
|
|
80
|
+
rest = next;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The bindings that are safe to remove when this capability goes: the ones no other installed
|
|
86
|
+
* capability still requires. A shared binding (e.g. the app `DB` used by auth, email, and audit) stays
|
|
87
|
+
* — removing it would break the capabilities that remain. The mirror of `add` not duplicating a
|
|
88
|
+
* shared binding.
|
|
89
|
+
*/
|
|
90
|
+
export function removableBindings(target: HasBindings, others: readonly HasBindings[]): BindingSpec[] {
|
|
91
|
+
const kept = new Set(others.flatMap((other) => other.requiredBindings.map((b) => `${b.type}:${b.name}`)));
|
|
92
|
+
return target.requiredBindings.filter((binding) => !kept.has(`${binding.type}:${binding.name}`));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One binding entry as wrangler writes it — only `binding` identifies it for removal. */
|
|
96
|
+
interface BindingEntry {
|
|
97
|
+
binding: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A wrangler stanza's binding arrays — the keys `remove` touches.
|
|
102
|
+
*
|
|
103
|
+
* Wider than `add`'s writer by two: `vectorize` and `workflows` are written by the capability's own
|
|
104
|
+
* provisioner rather than by `add` (they carry required fields only provisioning knows —
|
|
105
|
+
* `capabilities/add.ts` and `project/appBindings.ts` explain why), and removing a capability has to take
|
|
106
|
+
* them out whoever wrote them. Remove is the inverse of *the project's wiring*, not of one command.
|
|
107
|
+
*/
|
|
108
|
+
interface WranglerBindings {
|
|
109
|
+
d1_databases?: BindingEntry[];
|
|
110
|
+
kv_namespaces?: BindingEntry[];
|
|
111
|
+
r2_buckets?: BindingEntry[];
|
|
112
|
+
vectorize?: BindingEntry[];
|
|
113
|
+
ai?: BindingEntry;
|
|
114
|
+
workflows?: BindingEntry[];
|
|
115
|
+
durable_objects?: { bindings: { name: string; class_name: string }[] };
|
|
116
|
+
migrations?: { tag: string; new_sqlite_classes?: string[] }[];
|
|
117
|
+
env?: Record<string, WranglerBindings | undefined>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Drop every matching entry from an array **in place**. comment-json keeps an array's comments as
|
|
122
|
+
* symbol-keyed properties on the array object itself, so a `filter()` — a fresh, plain array —
|
|
123
|
+
* silently deletes them, and `pithy remove` would strip the adopter's notes along with the binding.
|
|
124
|
+
* Splice back-to-front so the surviving indices (and their comments) never shift under the loop.
|
|
125
|
+
* `feature/wranglerEnv.ts` documents the same rule for the id writer.
|
|
126
|
+
*/
|
|
127
|
+
function spliceWhere<Entry>(entries: Entry[], matches: (entry: Entry) => boolean): void {
|
|
128
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
129
|
+
const entry = entries[index];
|
|
130
|
+
if (entry !== undefined && matches(entry)) entries.splice(index, 1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Drop a set of bindings from one stanza in place (the per-environment keys). */
|
|
135
|
+
function stripBindings(stanza: WranglerBindings, bindings: BindingSpec[]): void {
|
|
136
|
+
const named = (type: BindingSpec["type"]): Set<string> =>
|
|
137
|
+
new Set(bindings.filter((binding) => binding.type === type).map((binding) => binding.name));
|
|
138
|
+
|
|
139
|
+
const d1 = named("d1");
|
|
140
|
+
const kv = named("kv");
|
|
141
|
+
const r2 = named("r2");
|
|
142
|
+
const vectorize = named("vectorize");
|
|
143
|
+
const workflows = named("workflow");
|
|
144
|
+
const ai = named("ai");
|
|
145
|
+
const durableObjects = named("durable_object");
|
|
146
|
+
|
|
147
|
+
if (stanza.d1_databases) spliceWhere(stanza.d1_databases, (entry) => d1.has(entry.binding));
|
|
148
|
+
if (stanza.kv_namespaces) spliceWhere(stanza.kv_namespaces, (entry) => kv.has(entry.binding));
|
|
149
|
+
if (stanza.r2_buckets) spliceWhere(stanza.r2_buckets, (entry) => r2.has(entry.binding));
|
|
150
|
+
if (stanza.vectorize) spliceWhere(stanza.vectorize, (entry) => vectorize.has(entry.binding));
|
|
151
|
+
if (stanza.workflows) spliceWhere(stanza.workflows, (entry) => workflows.has(entry.binding));
|
|
152
|
+
// `ai` is a single object, not an array — the mirror of add's `??=` is dropping the key outright.
|
|
153
|
+
if (stanza.ai && ai.has(stanza.ai.binding)) stanza.ai = undefined;
|
|
154
|
+
if (stanza.durable_objects?.bindings) {
|
|
155
|
+
spliceWhere(stanza.durable_objects.bindings, (entry) => durableObjects.has(entry.name));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Strip removed Durable Object classes from the **top-level** `migrations` tags — the mirror of
|
|
161
|
+
* `appendDurableObjectMigrations`, including the DO binding and its migration tag (the acceptance
|
|
162
|
+
* criterion that remove is the clean inverse). A tag left with no classes is dropped, and an empty
|
|
163
|
+
* `migrations` array is removed entirely, so a project with no DOs looks exactly as it did before add.
|
|
164
|
+
*/
|
|
165
|
+
function stripDurableObjectMigrations(config: WranglerBindings, bindings: BindingSpec[]): void {
|
|
166
|
+
const classes = new Set(
|
|
167
|
+
bindings.filter((b) => b.type === "durable_object" && b.className).map((b) => b.className as string),
|
|
168
|
+
);
|
|
169
|
+
if (classes.size === 0 || !config.migrations) return;
|
|
170
|
+
|
|
171
|
+
for (const migration of config.migrations) {
|
|
172
|
+
if (migration.new_sqlite_classes) spliceWhere(migration.new_sqlite_classes, (className) => classes.has(className));
|
|
173
|
+
}
|
|
174
|
+
spliceWhere(config.migrations, (migration) => (migration.new_sqlite_classes?.length ?? 0) === 0);
|
|
175
|
+
if (config.migrations.length === 0) config.migrations = undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Rewrite one Worker's `pithy.config.ts` with the capability's import + registration removed. */
|
|
179
|
+
export async function removeFromConfig(workerDir: string, name: string, pkg: string): Promise<void> {
|
|
180
|
+
const path = join(workerDir, "pithy.config.ts");
|
|
181
|
+
const source = await readFile(path, "utf8");
|
|
182
|
+
await writeFile(path, unwireConfig(source, name, pkg));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Remove the capability's bindings from every environment of one Worker's `wrangler.jsonc`,
|
|
187
|
+
* comment-preserving — keeping any binding another capability wired into **that Worker** still needs
|
|
188
|
+
* ({@link removableBindings}). Returns the binding names actually removed. A sibling Worker that wires
|
|
189
|
+
* the same capability keeps its own bindings; each Worker's wrangler.jsonc is independent.
|
|
190
|
+
*/
|
|
191
|
+
export async function removeFromWrangler(
|
|
192
|
+
workerDir: string,
|
|
193
|
+
target: HasBindings,
|
|
194
|
+
others: readonly HasBindings[],
|
|
195
|
+
): Promise<string[]> {
|
|
196
|
+
const bindings = removableBindings(target, others);
|
|
197
|
+
if (bindings.length === 0) return [];
|
|
198
|
+
|
|
199
|
+
const config = (await readWranglerConfig(workerDir)) as WranglerBindings;
|
|
200
|
+
stripBindings(config, bindings);
|
|
201
|
+
for (const stanza of Object.values(config.env ?? {})) {
|
|
202
|
+
if (stanza) stripBindings(stanza, bindings);
|
|
203
|
+
}
|
|
204
|
+
// DO class migrations are top-level only — strip them once, not per env.
|
|
205
|
+
stripDurableObjectMigrations(config, bindings);
|
|
206
|
+
await writeWranglerConfig(workerDir, config);
|
|
207
|
+
return bindings.map((binding) => binding.name);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Take the capability's Durable Object exports out of the Worker's entry — the inverse of the half of a
|
|
212
|
+
* DO binding that lives in the adopter's code (#428).
|
|
213
|
+
*
|
|
214
|
+
* Necessary rather than tidy: the export names a module this command is about to delete — the package it
|
|
215
|
+
* uninstalls, or the `capabilities/<cap>` fork it removes whole — so leaving it behind is a Worker that no
|
|
216
|
+
* longer builds, the same failure a left-behind import is, one file over. Returns the classes taken out.
|
|
217
|
+
*
|
|
218
|
+
* **Both spellings, by the rule the config's import already uses.** `pithy add` writes the export against
|
|
219
|
+
* the package and `pithy add --eject` repoints it into the fork, so the specifier that is ours to remove
|
|
220
|
+
* is either — which is exactly what {@link isCapabilityImport} answers, and why the capability's name is a
|
|
221
|
+
* parameter here. Anything else pointing at the same class is the adopter's own line and stays.
|
|
222
|
+
*
|
|
223
|
+
* A class another capability wired into **this** Worker still binds stays too, by
|
|
224
|
+
* {@link removableBindings}.
|
|
225
|
+
*/
|
|
226
|
+
export async function removeFromEntry(
|
|
227
|
+
workerDir: string,
|
|
228
|
+
capability: string,
|
|
229
|
+
target: HasBindings,
|
|
230
|
+
others: readonly HasBindings[],
|
|
231
|
+
): Promise<string[]> {
|
|
232
|
+
const exports = durableObjectExports(removableBindings(target, others));
|
|
233
|
+
if (exports.length === 0) return [];
|
|
234
|
+
|
|
235
|
+
// Nothing to unwire is not a failure here, unlike in `add`: a removal runs against whatever state the
|
|
236
|
+
// project is in, and stopping because the entry has already gone would strand the capability
|
|
237
|
+
// half-removed. An entry that is *there* and will not open is still a refusal — `readOptionalFile`
|
|
238
|
+
// draws that line, and this does not draw its own.
|
|
239
|
+
const path = await workerEntryPath(workerDir);
|
|
240
|
+
if (path === null) return [];
|
|
241
|
+
const source = await readOptionalFile(path);
|
|
242
|
+
if (source === null) return [];
|
|
243
|
+
|
|
244
|
+
// The fork as **the entry** reaches it — `../capabilities/<cap>` from the `src/` an entry usually sits
|
|
245
|
+
// in, where `pithy.config.ts` says `./capabilities/<cap>`. `--eject` writes each file its own spelling,
|
|
246
|
+
// so a predicate built from the Worker's directory would read the ejected line as somebody else's and
|
|
247
|
+
// leave a re-export of a directory this command is about to delete.
|
|
248
|
+
const written = withoutDurableObjectExports(source, exports, (specifier) =>
|
|
249
|
+
isCapabilityImport(
|
|
250
|
+
specifier,
|
|
251
|
+
capabilityPackageName(capability),
|
|
252
|
+
ejectSpecifierFromDir(workerDir, dirname(path), capability),
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
if (written === source) return [];
|
|
256
|
+
await writeFile(path, written);
|
|
257
|
+
return exports.filter((entry) => !exportsName(written, entry.className)).map((entry) => entry.className);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Whether a Worker's `pithy.config.ts` still imports the capability's package. Matches the module
|
|
262
|
+
* specifier itself, not only the import line `add` writes, so a hand-added deep import
|
|
263
|
+
* (`@pithy-sh/auth/src/routes`) counts too — uninstalling the package would break that Worker just the
|
|
264
|
+
* same. Ejected wiring (`./capabilities/<cap>`) never matches: that Worker owns its copy and needs no
|
|
265
|
+
* package.
|
|
266
|
+
*/
|
|
267
|
+
export function importsPackage(source: string, pkg: string): boolean {
|
|
268
|
+
return new RegExp(`["']${escapeRegExp(pkg)}(["']|/)`).test(source);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The sibling Workers that still import `pkg`. The capability package is installed **once at the project
|
|
273
|
+
* root** and shared by every Worker (`capabilities/flow.ts`), so it may only be uninstalled when no other
|
|
274
|
+
* Worker composes it — otherwise that Worker's `pithy.config.ts` stops loading and every command that
|
|
275
|
+
* reads it fails project-wide. The target Worker is excluded: it is the one being unwired.
|
|
276
|
+
*
|
|
277
|
+
* Configs are read as text, never imported: this runs mid-removal, and executing a config to answer
|
|
278
|
+
* "is the package still needed?" would be both slower and fragile.
|
|
279
|
+
*/
|
|
280
|
+
async function workersUsingPackage(projectDir: string, workerDir: string, pkg: string): Promise<string[]> {
|
|
281
|
+
const workers = await discoverWorkers(projectDir).catch(() => []);
|
|
282
|
+
const using: string[] = [];
|
|
283
|
+
for (const worker of workers) {
|
|
284
|
+
if (resolve(worker.dir) === resolve(workerDir)) continue;
|
|
285
|
+
const source = await readFile(join(worker.dir, "pithy.config.ts"), "utf8").catch(() => "");
|
|
286
|
+
if (importsPackage(source, pkg)) using.push(worker.name);
|
|
287
|
+
}
|
|
288
|
+
return using;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** True if a path exists. */
|
|
292
|
+
async function exists(path: string): Promise<boolean> {
|
|
293
|
+
try {
|
|
294
|
+
await stat(path);
|
|
295
|
+
return true;
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Whether the capability's tables outlive it — true unless a `--drop` reversed every migration. */
|
|
302
|
+
function tablesRemain(target: Capability | undefined, dropped: DatabaseRun[] | undefined): boolean {
|
|
303
|
+
const declaresMigrations = Object.values(target?.databases ?? {}).some(
|
|
304
|
+
(spec) => spec?.migrations && Object.keys(spec.migrations).length > 0,
|
|
305
|
+
);
|
|
306
|
+
if (!declaresMigrations) return false;
|
|
307
|
+
return !dropped || dropped.every((run) => run.results.length === 0) ? declaresMigrations : false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Injectable side effects, so the orchestration is testable without a real drop/uninstall/delete. */
|
|
311
|
+
export interface RemoveSteps {
|
|
312
|
+
/** Every capability wired into the target Worker (default: its loaded `pithy.config.ts`). */
|
|
313
|
+
loadCapabilities: () => Promise<Capability[]>;
|
|
314
|
+
/** Drop the removed capability's tables for an env (default: {@link dropCapabilityTables}). */
|
|
315
|
+
dropTables: (capability: Capability, env: string) => Promise<DatabaseRun[]>;
|
|
316
|
+
/**
|
|
317
|
+
* Uninstall the package (default: {@link uninstallPackage}). `uninstalled` is false when the step
|
|
318
|
+
* declined — a linked checkout provides the package, nothing declared it, and npm would prune the
|
|
319
|
+
* whole scope if asked to remove it.
|
|
320
|
+
*/
|
|
321
|
+
uninstall: (pkg: string) => Promise<{ packageManager: string; uninstalled: boolean }>;
|
|
322
|
+
/**
|
|
323
|
+
* Delete an ejected capability's local source tree — `apps/<worker>/capabilities/<cap>`, four segments
|
|
324
|
+
* composed out of names (default: `removeScaffoldPath`, which refuses a symlink at any component and a
|
|
325
|
+
* path resolving outside the project, then does the `rm` itself).
|
|
326
|
+
*
|
|
327
|
+
* The default said "recursive `fs.rm`" until #158, and that is what it was: a link at `apps` carried the
|
|
328
|
+
* delete onto whatever it pointed at, outside the project, with nothing to recover. A docstring
|
|
329
|
+
* advertising the ungated version is how the next producer of that bug gets written — a step handed in
|
|
330
|
+
* here is held to the same gate.
|
|
331
|
+
*/
|
|
332
|
+
deleteSource: (dir: string) => Promise<void>;
|
|
333
|
+
/** Whether `@pithy-sh/<cap>` is installed (default: a `node_modules` stat). */
|
|
334
|
+
packageInstalled: (pkg: string) => Promise<boolean>;
|
|
335
|
+
/**
|
|
336
|
+
* The **other** Workers that still import the package, by name — the guard on uninstalling a
|
|
337
|
+
* project-wide dependency (default: a scan of `apps/*`). Empty means the package is free to go.
|
|
338
|
+
*/
|
|
339
|
+
workersUsingPackage: (pkg: string) => Promise<string[]>;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** What {@link defaultRemoveSteps} needs: the two directories a removal spans, and the wired set. */
|
|
343
|
+
export interface DefaultRemoveStepsOptions {
|
|
344
|
+
/** The project root — where the lockfile and `node_modules` live. Packages are a project dependency. */
|
|
345
|
+
projectDir: string;
|
|
346
|
+
/** The target Worker's directory — its `wrangler.jsonc` names the D1 a `--drop` reverses. */
|
|
347
|
+
workerDir: string;
|
|
348
|
+
/** Every capability wired into the target Worker. */
|
|
349
|
+
loadCapabilities: () => Promise<Capability[]>;
|
|
350
|
+
/**
|
|
351
|
+
* The project name, resolved by the caller from the root `pithy.config.ts` (`requireProjectName`,
|
|
352
|
+
* never `resolveProjectName`) and handed over as a plain string. A `--drop` reverses migrations and
|
|
353
|
+
* drops tables, so it is checked against the database's recorded owner first: without it, `pithy
|
|
354
|
+
* remove --drop` reversed another project's migrations on a shared D1 and exited 0.
|
|
355
|
+
*/
|
|
356
|
+
project: string;
|
|
357
|
+
/**
|
|
358
|
+
* The Cloudflare account this project belongs to, from `projectCloudflareAccount(projectDir)`, or
|
|
359
|
+
* `null` when it names none. Required (#234): `--drop --env staging` reverses migrations against a
|
|
360
|
+
* live database, and `dropCapabilityTables`'s account was optional and omitted here, so it reversed
|
|
361
|
+
* them in whichever account `<config>/cloudflare.json` happened to hold credentials for.
|
|
362
|
+
*/
|
|
363
|
+
account: CloudflareAccountSelection | null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Real side effects for a live `pithy remove`. The split is deliberate: **wiring is per-Worker**
|
|
368
|
+
* (the tables dropped are the target Worker's D1, resolved from its own `wrangler.jsonc`) while the
|
|
369
|
+
* **package is project-wide** (one workspace dependency, installed and uninstalled at the root) — which
|
|
370
|
+
* is exactly why the uninstall is gated on {@link RemoveSteps.workersUsingPackage}.
|
|
371
|
+
*/
|
|
372
|
+
export function defaultRemoveSteps(options: DefaultRemoveStepsOptions): RemoveSteps {
|
|
373
|
+
const { projectDir, workerDir } = options;
|
|
374
|
+
return {
|
|
375
|
+
loadCapabilities: options.loadCapabilities,
|
|
376
|
+
dropTables: (capability, env) =>
|
|
377
|
+
dropCapabilityTables({
|
|
378
|
+
capability,
|
|
379
|
+
workerDir,
|
|
380
|
+
persistRoot: projectDir,
|
|
381
|
+
env,
|
|
382
|
+
project: options.project,
|
|
383
|
+
account: options.account,
|
|
384
|
+
}),
|
|
385
|
+
uninstall: (pkg) => uninstallPackage({ projectDir, pkg }),
|
|
386
|
+
// Gated (#158). This is `apps/<worker>/capabilities/<cap>` — four segments Pithy composed out of
|
|
387
|
+
// names — handed to a recursive delete. A symlink at any of them carried the delete onto whatever
|
|
388
|
+
// the link pointed at, outside the project, with nothing to recover afterwards. `removeScaffoldPath`
|
|
389
|
+
// refuses a link at every component and refuses a path that resolves outside `projectDir`, then does
|
|
390
|
+
// the `rm` itself, so the gate cannot be skipped by editing this line.
|
|
391
|
+
deleteSource: (dir) => removeScaffoldPath(projectDir, dir),
|
|
392
|
+
packageInstalled: (pkg) => exists(join(projectDir, "node_modules", pkg)),
|
|
393
|
+
workersUsingPackage: (pkg) => workersUsingPackage(projectDir, workerDir, pkg),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export interface RemoveCapabilityOptions {
|
|
398
|
+
/**
|
|
399
|
+
* The target Worker's directory (`apps/<name>`) — its `pithy.config.ts`, its `wrangler.jsonc`, and
|
|
400
|
+
* its `capabilities/` fork directory. Only this Worker is unwired.
|
|
401
|
+
*/
|
|
402
|
+
workerDir: string;
|
|
403
|
+
capability: string;
|
|
404
|
+
/**
|
|
405
|
+
* Drop the capability's tables for this env before unwiring; omit to leave data. `confirm` is asked
|
|
406
|
+
* once the capability is confirmed present and unblocked — a `false` return aborts with zero changes,
|
|
407
|
+
* so the confirmation gates the first (and only) destructive step.
|
|
408
|
+
*/
|
|
409
|
+
drop?: { env: string; confirm?: () => Promise<boolean> };
|
|
410
|
+
steps: RemoveSteps;
|
|
411
|
+
/** Audit emitter. Defaults to recording nothing, so a caller without audit wiring still works. */
|
|
412
|
+
audit?: CliAuditEmit;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** What `remove` did — surfaced to the user (this command has no `--json`). */
|
|
416
|
+
export interface RemoveResult {
|
|
417
|
+
capability: string;
|
|
418
|
+
/** False when nothing was wired or installed — a no-op. */
|
|
419
|
+
present: boolean;
|
|
420
|
+
ejected: boolean;
|
|
421
|
+
packageManager?: string;
|
|
422
|
+
dropped?: DatabaseRun[];
|
|
423
|
+
removedBindings: string[];
|
|
424
|
+
/**
|
|
425
|
+
* The other Workers that still compose the capability, so the shared package was **kept** installed.
|
|
426
|
+
* Empty when nothing else needs it — the case where `packageManager` names the uninstall that ran.
|
|
427
|
+
*/
|
|
428
|
+
keptFor: string[];
|
|
429
|
+
/**
|
|
430
|
+
* True when the package stayed because a linked checkout provides it. Nothing declared it, so there is
|
|
431
|
+
* nothing to uninstall — and npm, asked anyway, prunes every linked sibling with it. Distinct from
|
|
432
|
+
* {@link RemoveResult.keptFor}, which is another Worker still needing a package that *is* declared.
|
|
433
|
+
*/
|
|
434
|
+
keptLinked?: boolean;
|
|
435
|
+
/** True when the capability's D1 tables were left in place (no `--drop`). */
|
|
436
|
+
tablesRemain: boolean;
|
|
437
|
+
/** True when a `--drop` confirmation was declined — nothing was changed. */
|
|
438
|
+
aborted?: boolean;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The precise inverse of `add` (and `add --eject`), for **one Worker**. Detects the capability's form
|
|
443
|
+
* and reverses it: an optional `--drop` of its tables first (while its `down` code is still present),
|
|
444
|
+
* then unwires that Worker's config import + registration and removes its bindings from every
|
|
445
|
+
* `wrangler.jsonc` env, then uninstalls the package (package-served) or deletes the local source
|
|
446
|
+
* (ejected). Never destroys data unless `drop` is set. Idempotent: an absent capability is a no-op.
|
|
447
|
+
* Refuses when another capability wired into the same Worker depends on this one.
|
|
448
|
+
*
|
|
449
|
+
* The package is the one project-wide part of a removal, so it is uninstalled only when **no other
|
|
450
|
+
* Worker still imports it** — those Workers come back in {@link RemoveResult.keptFor}.
|
|
451
|
+
*/
|
|
452
|
+
export async function removeCapability(options: RemoveCapabilityOptions): Promise<RemoveResult> {
|
|
453
|
+
const { workerDir, capability, steps } = options;
|
|
454
|
+
const audit = options.audit ?? (async () => {});
|
|
455
|
+
// Resolved through the catalog, never interpolated from the name. `controlplane` ships inside
|
|
456
|
+
// `@pithy-sh/core`, and the interpolated form built a package that has never existed — which made
|
|
457
|
+
// `packageInstalled` and `workersUsingPackage` both answer for a phantom. The second is the dangerous
|
|
458
|
+
// one: `keptFor` came back empty always, so the guard that refuses `--drop` while a sibling Worker
|
|
459
|
+
// still wires the capability never fired, and dropping the seam's tables from one Worker would have
|
|
460
|
+
// deleted every registered public key out from under another Worker still serving control-plane
|
|
461
|
+
// routes against them.
|
|
462
|
+
const pkg = capabilityPackageName(capability);
|
|
463
|
+
|
|
464
|
+
const capabilities = await steps.loadCapabilities();
|
|
465
|
+
const target = capabilities.find((c) => c.name === capability);
|
|
466
|
+
const ejected = await isEjected(workerDir, capability);
|
|
467
|
+
const installed = await steps.packageInstalled(pkg);
|
|
468
|
+
|
|
469
|
+
if (!target && !ejected && !installed) {
|
|
470
|
+
return { capability, present: false, ejected: false, removedBindings: [], keptFor: [], tablesRemain: false };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const dependents = capabilities
|
|
474
|
+
.filter((c) => c.name !== capability && c.dependsOn?.includes(capability))
|
|
475
|
+
.map((c) => c.name);
|
|
476
|
+
if (dependents.length > 0) {
|
|
477
|
+
await audit({
|
|
478
|
+
action: "capability/removed",
|
|
479
|
+
outcome: "failure",
|
|
480
|
+
severity: "warning",
|
|
481
|
+
resourceType: "capability",
|
|
482
|
+
resourceId: capability,
|
|
483
|
+
metadata: { reason: "dependents", dependents },
|
|
484
|
+
});
|
|
485
|
+
throw new ConflictError({
|
|
486
|
+
message: `Can't remove ${capability} — ${dependents.join(", ")} depend${dependents.length === 1 ? "s" : ""} on it.`,
|
|
487
|
+
action: `Remove ${dependents.join(", ")} first, then remove ${capability}.`,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Which sibling Workers still wire this package. Resolved once, up front, because it gates BOTH the
|
|
492
|
+
// drop and the uninstall: two Workers that both compose a capability against the same D1 share its
|
|
493
|
+
// tables, so reversing its migrations for one Worker would delete data the other is still serving.
|
|
494
|
+
// An ejected capability is that Worker's own forked copy, so no sibling can be relying on the package.
|
|
495
|
+
const keptFor = ejected ? [] : await steps.workersUsingPackage(pkg);
|
|
496
|
+
|
|
497
|
+
if (options.drop && keptFor.length > 0) {
|
|
498
|
+
const plural = keptFor.length === 1 ? "s" : "";
|
|
499
|
+
await audit({
|
|
500
|
+
environment: options.drop.env,
|
|
501
|
+
action: "capability/tables_dropped",
|
|
502
|
+
outcome: "failure",
|
|
503
|
+
severity: "warning",
|
|
504
|
+
resourceType: "capability",
|
|
505
|
+
resourceId: capability,
|
|
506
|
+
metadata: { reason: "shared_with_workers", keptFor },
|
|
507
|
+
});
|
|
508
|
+
throw new ConflictError({
|
|
509
|
+
message: `Can't drop ${capability}'s tables — ${keptFor.join(", ")} still wire${plural} it.`,
|
|
510
|
+
action: `Remove ${capability} from ${keptFor.join(", ")} first, or re-run without --drop to unwire this worker and keep the data.`,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Drop tables first: the down code lives in the source about to be unwired and uninstalled. The
|
|
515
|
+
// confirmation gates it — a decline aborts here, before any file has changed.
|
|
516
|
+
let dropped: DatabaseRun[] | undefined;
|
|
517
|
+
if (options.drop && target) {
|
|
518
|
+
if (options.drop.confirm && !(await options.drop.confirm())) {
|
|
519
|
+
// `denied`, not `failure`: a human deliberately blocked a destructive action. First-class in the trail.
|
|
520
|
+
await audit({
|
|
521
|
+
environment: options.drop.env,
|
|
522
|
+
action: "capability/tables_dropped",
|
|
523
|
+
outcome: "denied",
|
|
524
|
+
severity: "warning",
|
|
525
|
+
resourceType: "capability",
|
|
526
|
+
resourceId: capability,
|
|
527
|
+
});
|
|
528
|
+
return {
|
|
529
|
+
capability,
|
|
530
|
+
present: true,
|
|
531
|
+
ejected,
|
|
532
|
+
removedBindings: [],
|
|
533
|
+
keptFor: [],
|
|
534
|
+
tablesRemain: true,
|
|
535
|
+
aborted: true,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
dropped = await steps.dropTables(target, options.drop.env);
|
|
539
|
+
const migrationsReverted = dropped.reduce((sum, run) => sum + run.results.length, 0);
|
|
540
|
+
await audit({
|
|
541
|
+
environment: options.drop.env,
|
|
542
|
+
action: "capability/tables_dropped",
|
|
543
|
+
outcome: "success",
|
|
544
|
+
severity: "warning",
|
|
545
|
+
resourceType: "capability",
|
|
546
|
+
resourceId: capability,
|
|
547
|
+
metadata: { migrationsReverted },
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
await removeFromConfig(workerDir, capability, pkg);
|
|
552
|
+
const others = capabilities.filter((c) => c.name !== capability);
|
|
553
|
+
const removedBindings = target ? await removeFromWrangler(workerDir, target, others) : [];
|
|
554
|
+
if (target) await removeFromEntry(workerDir, capability, target, others);
|
|
555
|
+
|
|
556
|
+
let packageManager: string | undefined;
|
|
557
|
+
let keptLinked = false;
|
|
558
|
+
if (ejected) {
|
|
559
|
+
await steps.deleteSource(join(workerDir, EJECT_DIR, capability));
|
|
560
|
+
} else if (installed && keptFor.length === 0 && !isSharedCapabilityPackage(pkg)) {
|
|
561
|
+
// One install at the root, shared by every Worker. Uninstalling it while a sibling Worker still
|
|
562
|
+
// imports it would break that Worker's pithy.config.ts — and every command that loads it. So the
|
|
563
|
+
// wiring goes and the package stays, which a later `pithy remove` from the last Worker cleans up.
|
|
564
|
+
//
|
|
565
|
+
// A shared package is never uninstalled at all. `controlplane` lives in `@pithy-sh/core`, which is
|
|
566
|
+
// every capability's dependency and the runtime the app is built on: removing the seam unwires it,
|
|
567
|
+
// and uninstalling core would take the project with it.
|
|
568
|
+
//
|
|
569
|
+
// The step may still decline: a package a linked checkout provides has no declaration to remove, and
|
|
570
|
+
// npm asked to remove one prunes every linked sibling as extraneous. Only a removal that happened is
|
|
571
|
+
// reported as one — the manager is dropped rather than kept, so no surface can claim otherwise.
|
|
572
|
+
const outcome = await steps.uninstall(pkg);
|
|
573
|
+
if (outcome.uninstalled) packageManager = outcome.packageManager;
|
|
574
|
+
else keptLinked = true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
await audit({
|
|
578
|
+
action: "capability/removed",
|
|
579
|
+
outcome: "success",
|
|
580
|
+
severity: "info",
|
|
581
|
+
resourceType: "capability",
|
|
582
|
+
resourceId: capability,
|
|
583
|
+
metadata: { ejected, packageManager: packageManager ?? null, removedBindings, keptFor, keptLinked },
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
capability,
|
|
588
|
+
present: true,
|
|
589
|
+
ejected,
|
|
590
|
+
packageManager,
|
|
591
|
+
dropped,
|
|
592
|
+
removedBindings,
|
|
593
|
+
keptFor,
|
|
594
|
+
keptLinked,
|
|
595
|
+
tablesRemain: tablesRemain(target, dropped),
|
|
596
|
+
};
|
|
597
|
+
}
|