@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,238 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { readdir } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { CapabilityManifest } from "@pithy-sh/core/src/capability/manifest";
|
|
7
|
+
import { InternalError, messageOf, NotFoundError, type PithyError } from "@pithy-sh/core/src/error/pithyError";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { readFileOutcome } from "../project/readOptionalFile";
|
|
10
|
+
import { type CatalogEntry, capabilityPackageDir } from "./catalog";
|
|
11
|
+
|
|
12
|
+
/** Every capability ships under this npm scope; the CLI resolves them by name. */
|
|
13
|
+
const SCOPE = "@pithy-sh";
|
|
14
|
+
/** The manifest file each capability package ships at its root (strict JSON). */
|
|
15
|
+
const MANIFEST_FILE = "pithy.manifest.json";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Where a capability's manifest lands once installed into the project.
|
|
19
|
+
*
|
|
20
|
+
* The directory comes from the catalog's `package`, not from the capability name. Almost always they
|
|
21
|
+
* agree — but `controlplane` ships inside `@pithy-sh/core`, so a name-derived path would look in a
|
|
22
|
+
* package that does not exist. Deriving it from the one field that already records where a capability
|
|
23
|
+
* lives means the exception is stated once (see {@link CatalogEntry.package}) rather than mirrored here.
|
|
24
|
+
*/
|
|
25
|
+
function manifestPath(projectDir: string, name: string): string {
|
|
26
|
+
return join(projectDir, "node_modules", SCOPE, capabilityPackageDir(name), MANIFEST_FILE);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What reading one package's `pithy.manifest.json` produced.
|
|
31
|
+
*
|
|
32
|
+
* The three answers are three different facts, and collapsing any two of them is what this file kept
|
|
33
|
+
* getting wrong. "The package ships no manifest" is ordinary — `@pithy-sh/cli` is not a capability. "The
|
|
34
|
+
* manifest is there and unusable" is a defect in someone's package, and it has to be said out loud. They
|
|
35
|
+
* were one `catch` for as long as this code existed (#184).
|
|
36
|
+
*/
|
|
37
|
+
type ManifestRead =
|
|
38
|
+
/** The package ships a manifest, and it parses. */
|
|
39
|
+
| { readonly state: "manifest"; readonly manifest: CapabilityManifest }
|
|
40
|
+
/** The package ships no manifest at all — the one case that may be skipped in silence. */
|
|
41
|
+
| { readonly state: "absent" }
|
|
42
|
+
/** The manifest is there and unusable: unreadable, unparseable, or refused by the schema. */
|
|
43
|
+
| { readonly state: "invalid"; readonly cause: unknown };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read and validate one package's manifest, distinguishing "not there" from "there and broken".
|
|
47
|
+
*
|
|
48
|
+
* **Only `ENOENT` means the package ships no manifest**, and that decision is {@link readFileOutcome}'s
|
|
49
|
+
* now (`../project/readOptionalFile.ts`) rather than a third local copy of it. Reading any other errno as
|
|
50
|
+
* absence is how a present-but-unreadable capability disappears from `pithy add --list` with nothing said
|
|
51
|
+
* (#184); the primitive is where that sentence and its three producers live.
|
|
52
|
+
*
|
|
53
|
+
* The read is taken as a *value* and not as a throw, because the caller decides what a fault means:
|
|
54
|
+
* `loadManifest` was asked for this capability and refuses; `availableManifests` was asked for all of
|
|
55
|
+
* them and must still answer for the rest. Node's own error rides along as `cause`, so {@link faults}
|
|
56
|
+
* reports the errno an adopter can act on.
|
|
57
|
+
*/
|
|
58
|
+
async function readInstalledManifest(path: string): Promise<ManifestRead> {
|
|
59
|
+
const read = await readFileOutcome(path);
|
|
60
|
+
if (read.state === "absent") return { state: "absent" };
|
|
61
|
+
if (read.state === "unreadable") return { state: "invalid", cause: read.cause };
|
|
62
|
+
try {
|
|
63
|
+
return { state: "manifest", manifest: CapabilityManifest.parse(JSON.parse(read.text)) };
|
|
64
|
+
} catch (cause) {
|
|
65
|
+
return { state: "invalid", cause };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve a capability's manifest by reading `pithy.manifest.json` from the installed
|
|
71
|
+
* `@pithy-sh/<name>` package. Every failure is a `PithyError` naming the capability.
|
|
72
|
+
*
|
|
73
|
+
* **Three refusals, because there are three failures**, and the rule joining them is one sentence: a
|
|
74
|
+
* refusal never names the command that has just run. A manifest that is there and will not open is not
|
|
75
|
+
* "not installed" — telling an adopter to run `pithy add auth` when the file is unreadable sends them to
|
|
76
|
+
* the command that just declined to run. Neither is a package that is installed and ships no manifest;
|
|
77
|
+
* see {@link absenceRefusal}, which is the half of that sentence #415 paid for.
|
|
78
|
+
*/
|
|
79
|
+
export async function loadManifest(name: string, projectDir: string): Promise<CapabilityManifest> {
|
|
80
|
+
const read = await readInstalledManifest(manifestPath(projectDir, name));
|
|
81
|
+
if (read.state === "manifest") return read.manifest;
|
|
82
|
+
if (read.state === "absent") throw await absenceRefusal(name, projectDir);
|
|
83
|
+
throw new InternalError({
|
|
84
|
+
message: `${SCOPE}/${capabilityPackageDir(name)} ships a malformed ${MANIFEST_FILE}${firstFault(read.cause)}`,
|
|
85
|
+
action: "Reinstall the capability, or report this to its maintainer.",
|
|
86
|
+
detail: faults(read.cause),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The refusal for a manifest that is not there — **two different refusals, told apart by whether the
|
|
92
|
+
* package is.**
|
|
93
|
+
*
|
|
94
|
+
* `runAdd` installs the package and *then* reads its manifest, so this branch is reached with the
|
|
95
|
+
* package already in `node_modules`. One sentence covered both cases for as long as the code existed,
|
|
96
|
+
* and it was the wrong one for exactly the case that can be reached from `pithy add`: telling an adopter
|
|
97
|
+
* to run `pithy add rating` is a dead end when `pithy add rating` is what they just ran and what
|
|
98
|
+
* installed the package this is complaining about. `@pithy-sh/matchmaking` and `@pithy-sh/rating` were
|
|
99
|
+
* complete, installable, and unaddable behind that answer (#415).
|
|
100
|
+
*
|
|
101
|
+
* A package that is there and ships no manifest is a defect in the package — either it is not a
|
|
102
|
+
* capability at all, or its author left the one file that makes a capability addable out of the publish.
|
|
103
|
+
* `capabilities/addable.test.ts` is what keeps that from being one of ours again.
|
|
104
|
+
*/
|
|
105
|
+
async function absenceRefusal(name: string, projectDir: string): Promise<PithyError> {
|
|
106
|
+
const pkg = `${SCOPE}/${capabilityPackageDir(name)}`;
|
|
107
|
+
if (!(await isInstalled(projectDir, name))) {
|
|
108
|
+
return new NotFoundError({
|
|
109
|
+
message: `No capability named "${name}" is installed.`,
|
|
110
|
+
action: `Run pithy add ${name} to install it.`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// `core/not_found`, the same code the other branch answers — the classification was never the bug and
|
|
114
|
+
// only the sentence moved. A package that is installed and is not a capability is an adopter naming the
|
|
115
|
+
// wrong thing (`pithy add cloudflare` finds a real @pithy-sh package that ships no manifest), so a 500
|
|
116
|
+
// would send them to *our* logs for an answer that is not in them, and would leave a `--json` caller
|
|
117
|
+
// unable to tell an ordinary mistake from a Pithy defect.
|
|
118
|
+
return new NotFoundError({
|
|
119
|
+
message: `${pkg} is installed and ships no ${MANIFEST_FILE}, so there is no capability named "${name}" to wire.`,
|
|
120
|
+
action: `Check the name against pithy add --list. If ${pkg} is meant to be a capability, report the missing ${MANIFEST_FILE} to its maintainer.`,
|
|
121
|
+
detail: `${manifestPath(projectDir, name)} does not exist, but the package directory does`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Whether the package behind a capability name is installed at all.
|
|
127
|
+
*
|
|
128
|
+
* Asked of `package.json` rather than of the directory: npm and bun both leave empty directories behind,
|
|
129
|
+
* and an empty one is not an install. Read through {@link readFileOutcome} so the module's one rule
|
|
130
|
+
* holds here too — only `ENOENT` is absence, and a `package.json` that is there and will not open is a
|
|
131
|
+
* package that is there.
|
|
132
|
+
*/
|
|
133
|
+
async function isInstalled(projectDir: string, name: string): Promise<boolean> {
|
|
134
|
+
const read = await readFileOutcome(
|
|
135
|
+
join(projectDir, "node_modules", SCOPE, capabilityPackageDir(name), "package.json"),
|
|
136
|
+
);
|
|
137
|
+
return read.state !== "absent";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A Zod issue path as a manifest reader would write it: `configOptions[2].key`, not `configOptions.2.key`. */
|
|
141
|
+
function fieldPath(path: readonly PropertyKey[]): string {
|
|
142
|
+
if (path.length === 0) return "<root>";
|
|
143
|
+
return path.reduce<string>(
|
|
144
|
+
(text, segment) =>
|
|
145
|
+
typeof segment === "number"
|
|
146
|
+
? `${text}[${segment}]`
|
|
147
|
+
: text === ""
|
|
148
|
+
? String(segment)
|
|
149
|
+
: `${text}.${String(segment)}`,
|
|
150
|
+
"",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A manifest's faults, one per line as `<path>: <why>` — the `detail` a refusal carries.
|
|
156
|
+
*
|
|
157
|
+
* A `ZodError`'s own `message` is the issue array as JSON, which buries the one sentence that says what
|
|
158
|
+
* is wrong. The path is what names the option: `configOptions[2].key` is the third option, and #174's
|
|
159
|
+
* messages carry the offending key or rationale verbatim beside it.
|
|
160
|
+
*/
|
|
161
|
+
function faults(cause: unknown): string {
|
|
162
|
+
if (!(cause instanceof z.ZodError)) return messageOf(cause);
|
|
163
|
+
return cause.issues.map((issue) => `${fieldPath(issue.path)}: ${issue.message}`).join("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The first fault, as a clause for the refusal's `message`.
|
|
168
|
+
*
|
|
169
|
+
* The manifest is named in the message and the option must be too — a capability with a dozen options
|
|
170
|
+
* and one bad key is otherwise a refusal that says only "malformed". The rest of the faults stay in
|
|
171
|
+
* `detail`, which is where throw-site context belongs (CLAUDE.md §Errors).
|
|
172
|
+
*/
|
|
173
|
+
function firstFault(cause: unknown): string {
|
|
174
|
+
const issue = cause instanceof z.ZodError ? cause.issues[0] : undefined;
|
|
175
|
+
if (!issue) return ".";
|
|
176
|
+
return `: ${fieldPath(issue.path)} — ${issue.message}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* An installed package whose manifest is there and unusable — named, with the reason, for whoever is
|
|
181
|
+
* reporting. A capability with one of these is missing from every answer this module gives, and the
|
|
182
|
+
* point of the type is that it can no longer be missing silently.
|
|
183
|
+
*/
|
|
184
|
+
export const ManifestFault = z
|
|
185
|
+
.object({
|
|
186
|
+
package: z.string().describe("The package the manifest was read from, as an adopter names it: @pithy-sh/audit."),
|
|
187
|
+
reason: z
|
|
188
|
+
.string()
|
|
189
|
+
.describe(
|
|
190
|
+
"Why it could not be used: the schema's own refusal text where the schema refused it — the same sentence loadManifest gives on the direct path — or the errno where the file would not open.",
|
|
191
|
+
),
|
|
192
|
+
})
|
|
193
|
+
.describe("An installed package whose pithy.manifest.json is present and unusable, named with the reason.");
|
|
194
|
+
export type ManifestFault = z.infer<typeof ManifestFault>;
|
|
195
|
+
|
|
196
|
+
/** What a scan of `node_modules/@pithy-sh` found: the manifests it could use, and the ones it could not. */
|
|
197
|
+
export interface AvailableManifests {
|
|
198
|
+
/** Every installed capability's validated manifest, in directory order. */
|
|
199
|
+
manifests: CapabilityManifest[];
|
|
200
|
+
/** Packages shipping a manifest that is present and unusable. Empty on a healthy install. */
|
|
201
|
+
faults: ManifestFault[];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Every installed capability's manifest, validated — what `pithy add --list`
|
|
206
|
+
* cross-references against the catalog. Scans `node_modules/@pithy-sh/*` and
|
|
207
|
+
* skips packages that ship no manifest (cli). Empty when nothing's installed.
|
|
208
|
+
*
|
|
209
|
+
* Core does ship one now: the `control-plane` seam is a real capability living inside it, so the
|
|
210
|
+
* directory scan finds `@pithy-sh/core` and reads it out as `controlplane`. Nothing here special-cases
|
|
211
|
+
* that — the scan reads whatever manifest it finds, and the name comes from the file.
|
|
212
|
+
*
|
|
213
|
+
* **A package shipping no manifest is skipped; a package shipping a broken one is reported.** This used
|
|
214
|
+
* to be one `catch` around both, so a manifest the schema refused made the capability vanish from
|
|
215
|
+
* `pithy add --list`, `pithy upgrade` and `pithy doctor` with no message anywhere — the three commands
|
|
216
|
+
* most likely to be run when something is missing were the three that stayed silent (#184). The faults
|
|
217
|
+
* ride back with the manifests rather than throwing, because one broken package must not take the
|
|
218
|
+
* listing of the other fifteen with it.
|
|
219
|
+
*/
|
|
220
|
+
export async function availableManifests(projectDir: string): Promise<AvailableManifests> {
|
|
221
|
+
const scopeDir = join(projectDir, "node_modules", SCOPE);
|
|
222
|
+
let entries: string[];
|
|
223
|
+
try {
|
|
224
|
+
entries = await readdir(scopeDir);
|
|
225
|
+
} catch {
|
|
226
|
+
return { manifests: [], faults: [] }; // no node_modules/@pithy-sh — nothing installed
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const manifests: CapabilityManifest[] = [];
|
|
230
|
+
const broken: ManifestFault[] = [];
|
|
231
|
+
for (const name of entries) {
|
|
232
|
+
const read = await readInstalledManifest(join(scopeDir, name, MANIFEST_FILE));
|
|
233
|
+
if (read.state === "manifest") manifests.push(read.manifest);
|
|
234
|
+
// Absent is ordinary — `@pithy-sh/cli` is not a capability — and stays silent, as it always has.
|
|
235
|
+
if (read.state === "invalid") broken.push({ package: `${SCOPE}/${name}`, reason: faults(read.cause) });
|
|
236
|
+
}
|
|
237
|
+
return { manifests, faults: broken };
|
|
238
|
+
}
|