@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.
Files changed (234) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +72 -0
  3. package/scripts/templateManifest.ts +49 -0
  4. package/scripts/tsconfig.json +26 -0
  5. package/scripts/vendorTemplate.ts +84 -0
  6. package/scripts/verifyPack.ts +88 -0
  7. package/src/audit/cliAudit.ts +406 -0
  8. package/src/bin.ts +111 -0
  9. package/src/capabilities/add.ts +288 -0
  10. package/src/capabilities/addBootstrap.ts +275 -0
  11. package/src/capabilities/catalog.ts +175 -0
  12. package/src/capabilities/compose.ts +39 -0
  13. package/src/capabilities/configConstants.ts +74 -0
  14. package/src/capabilities/configImports.ts +397 -0
  15. package/src/capabilities/eject.ts +331 -0
  16. package/src/capabilities/emailProvisioner.ts +346 -0
  17. package/src/capabilities/entitlementGap.ts +70 -0
  18. package/src/capabilities/entryExports.ts +162 -0
  19. package/src/capabilities/flow.ts +550 -0
  20. package/src/capabilities/hostRegistry.ts +368 -0
  21. package/src/capabilities/loadFailure.ts +208 -0
  22. package/src/capabilities/manifests.ts +238 -0
  23. package/src/capabilities/mediaProvisioner.ts +471 -0
  24. package/src/capabilities/mintSecrets.ts +306 -0
  25. package/src/capabilities/paymentsProvisioner.ts +207 -0
  26. package/src/capabilities/prerequisites.ts +168 -0
  27. package/src/capabilities/r2Bucket.ts +113 -0
  28. package/src/capabilities/reconcile.ts +1483 -0
  29. package/src/capabilities/remove.ts +597 -0
  30. package/src/capabilities/requiredOptions.ts +92 -0
  31. package/src/capabilities/rotateSecrets.ts +305 -0
  32. package/src/capabilities/secrets.ts +178 -0
  33. package/src/capabilities/secretsDispatcher.ts +29 -0
  34. package/src/capabilities/secretsProvisioner.ts +389 -0
  35. package/src/capabilities/storageProvisioner.ts +414 -0
  36. package/src/capabilities/supportProvisioner.ts +515 -0
  37. package/src/capabilities/testersLoader.ts +52 -0
  38. package/src/capabilities/testersProvisioner.ts +236 -0
  39. package/src/capabilities/turnstileProvisioner.ts +347 -0
  40. package/src/capabilities/vectorProvisioner.ts +260 -0
  41. package/src/ci/fileModes.ts +223 -0
  42. package/src/ci/sourceFiles.ts +200 -0
  43. package/src/ci/workflowDrivers.ts +524 -0
  44. package/src/cloudflare/accountAnswer.ts +110 -0
  45. package/src/cloudflare/config.ts +685 -0
  46. package/src/cloudflare/storeId.ts +129 -0
  47. package/src/commands/add.ts +372 -0
  48. package/src/commands/alias.ts +205 -0
  49. package/src/commands/dashboard.ts +651 -0
  50. package/src/commands/deploy.ts +150 -0
  51. package/src/commands/dev.ts +37 -0
  52. package/src/commands/doctor.ts +2059 -0
  53. package/src/commands/email.ts +425 -0
  54. package/src/commands/env.ts +155 -0
  55. package/src/commands/feature.ts +359 -0
  56. package/src/commands/init.ts +538 -0
  57. package/src/commands/media.ts +303 -0
  58. package/src/commands/migrate.ts +129 -0
  59. package/src/commands/payments.ts +336 -0
  60. package/src/commands/provision.ts +368 -0
  61. package/src/commands/remove.ts +151 -0
  62. package/src/commands/secrets.ts +652 -0
  63. package/src/commands/seed.ts +229 -0
  64. package/src/commands/storage.ts +309 -0
  65. package/src/commands/support.ts +331 -0
  66. package/src/commands/testers.ts +1020 -0
  67. package/src/commands/token.ts +364 -0
  68. package/src/commands/turnstile.ts +271 -0
  69. package/src/commands/ui.ts +222 -0
  70. package/src/commands/upgrade.ts +517 -0
  71. package/src/commands/vector.ts +390 -0
  72. package/src/commands/worker.ts +295 -0
  73. package/src/dashboard/api.ts +323 -0
  74. package/src/dashboard/connect.ts +758 -0
  75. package/src/dashboard/contract.ts +289 -0
  76. package/src/dashboard/grant.ts +124 -0
  77. package/src/dashboard/registry.ts +519 -0
  78. package/src/dashboard/resolveTarget.ts +119 -0
  79. package/src/dev/delivery.ts +174 -0
  80. package/src/dev/devLogin.ts +155 -0
  81. package/src/dev/devLoginTargets.ts +91 -0
  82. package/src/dev/env.ts +206 -0
  83. package/src/dev/hostWorkers.ts +290 -0
  84. package/src/dev/keys.ts +111 -0
  85. package/src/dev/logging.ts +87 -0
  86. package/src/dev/openUrl.ts +75 -0
  87. package/src/dev/orchestrator.ts +1014 -0
  88. package/src/dev/ports.ts +220 -0
  89. package/src/dev/readyWatch.ts +142 -0
  90. package/src/dev/state.ts +90 -0
  91. package/src/devSecrets/bootstrapVars.ts +265 -0
  92. package/src/devSecrets/devVars.ts +240 -0
  93. package/src/devSecrets/edit.ts +256 -0
  94. package/src/devSecrets/file.ts +277 -0
  95. package/src/devSecrets/generate.ts +428 -0
  96. package/src/devSecrets/location.ts +80 -0
  97. package/src/devSecrets/mode.ts +71 -0
  98. package/src/devSecrets/records.ts +30 -0
  99. package/src/devSecrets/report.ts +99 -0
  100. package/src/devSecrets/seed.ts +344 -0
  101. package/src/devSecrets/store.ts +262 -0
  102. package/src/devSecrets/targets.ts +204 -0
  103. package/src/dispatch.ts +147 -0
  104. package/src/docs/catalog.ts +246 -0
  105. package/src/docs/writeCatalog.ts +45 -0
  106. package/src/doctor/cloudflare.ts +287 -0
  107. package/src/doctor/devPreferences.ts +155 -0
  108. package/src/doctor/devSecrets.ts +464 -0
  109. package/src/doctor/devVars.ts +414 -0
  110. package/src/doctor/devVarsLocal.ts +138 -0
  111. package/src/doctor/environments.ts +155 -0
  112. package/src/doctor/health.ts +354 -0
  113. package/src/doctor/localDelivery.ts +91 -0
  114. package/src/doctor/portsRegistry.ts +252 -0
  115. package/src/doctor/projectName.ts +584 -0
  116. package/src/doctor/secretBindings.ts +166 -0
  117. package/src/doctor/settings.ts +274 -0
  118. package/src/doctor/settingsSources.ts +202 -0
  119. package/src/doctor/workerName.ts +174 -0
  120. package/src/doctor/wranglerVars.ts +33 -0
  121. package/src/feature/bindings.ts +93 -0
  122. package/src/feature/create.ts +179 -0
  123. package/src/feature/destroy.ts +160 -0
  124. package/src/feature/devConfig.ts +201 -0
  125. package/src/feature/identity.ts +100 -0
  126. package/src/feature/manifest.ts +132 -0
  127. package/src/feature/ports.ts +615 -0
  128. package/src/feature/provision.ts +362 -0
  129. package/src/feature/sync.ts +148 -0
  130. package/src/feature/worktree.ts +282 -0
  131. package/src/help/groups.ts +47 -0
  132. package/src/help/rootUsage.ts +135 -0
  133. package/src/main.ts +73 -0
  134. package/src/migrations/ledger.ts +129 -0
  135. package/src/migrations/registry.ts +47 -0
  136. package/src/migrations/run.ts +1066 -0
  137. package/src/notifier/check.ts +129 -0
  138. package/src/notifier/installer.ts +48 -0
  139. package/src/notifier/notify.ts +152 -0
  140. package/src/notifier/state.ts +248 -0
  141. package/src/notifier/version.ts +59 -0
  142. package/src/platform/editor.ts +333 -0
  143. package/src/platform/rc.ts +118 -0
  144. package/src/platform/shell.ts +83 -0
  145. package/src/project/appBindings.ts +184 -0
  146. package/src/project/appWorkflows.ts +266 -0
  147. package/src/project/applyDomains.ts +166 -0
  148. package/src/project/askDomains.ts +220 -0
  149. package/src/project/atomic.ts +466 -0
  150. package/src/project/bindingEntries.ts +425 -0
  151. package/src/project/config.ts +701 -0
  152. package/src/project/dashboard.ts +118 -0
  153. package/src/project/deploy.ts +364 -0
  154. package/src/project/devVars.ts +113 -0
  155. package/src/project/domainPrompt.ts +191 -0
  156. package/src/project/domains.ts +386 -0
  157. package/src/project/envInventory.ts +356 -0
  158. package/src/project/environment.ts +125 -0
  159. package/src/project/extensions.ts +69 -0
  160. package/src/project/jsonc.ts +289 -0
  161. package/src/project/packageManager.ts +238 -0
  162. package/src/project/readOptionalFile.ts +342 -0
  163. package/src/project/rollback.ts +145 -0
  164. package/src/project/scaffold.ts +1088 -0
  165. package/src/project/templateFiles.ts +53 -0
  166. package/src/project/verifyDeploy.ts +230 -0
  167. package/src/project/versionMetadata.ts +77 -0
  168. package/src/project/workerAddress.ts +176 -0
  169. package/src/project/workerCommand.ts +564 -0
  170. package/src/project/workerIdentity.ts +50 -0
  171. package/src/project/workerManifest.ts +135 -0
  172. package/src/project/workerScaffold.ts +289 -0
  173. package/src/project/workerScope.ts +394 -0
  174. package/src/project/workers.ts +86 -0
  175. package/src/project/workflows.ts +281 -0
  176. package/src/project/wrangler.ts +168 -0
  177. package/src/provision/confirm.ts +86 -0
  178. package/src/provision/environment.ts +407 -0
  179. package/src/provision/featureConfig.ts +98 -0
  180. package/src/provision/mode.ts +62 -0
  181. package/src/provision/pendingSecrets.ts +96 -0
  182. package/src/provision/resources.ts +126 -0
  183. package/src/provision/secretBindings.ts +149 -0
  184. package/src/provision/store.ts +33 -0
  185. package/src/provision/unprovisioned.ts +114 -0
  186. package/src/provision/wranglerEnv.ts +220 -0
  187. package/src/rootFlags.ts +48 -0
  188. package/src/seed/drivers.ts +423 -0
  189. package/src/seed/media.ts +187 -0
  190. package/src/seed/plan.ts +137 -0
  191. package/src/seed/prepare.ts +224 -0
  192. package/src/seed/registry.ts +25 -0
  193. package/src/seed/run.ts +793 -0
  194. package/src/seed/safety.ts +206 -0
  195. package/src/terminal/logger.ts +42 -0
  196. package/src/terminal/output.ts +64 -0
  197. package/src/terminal/style.ts +132 -0
  198. package/src/test-utils/doctorHarness.ts +190 -0
  199. package/src/test-utils/migrateHarness.ts +126 -0
  200. package/src/test-utils/seedHarness.ts +173 -0
  201. package/src/test-utils/tempRepo.ts +45 -0
  202. package/src/tokens/config.ts +16 -0
  203. package/src/tokens/engine.ts +345 -0
  204. package/src/tokens/mintedTokens.ts +233 -0
  205. package/src/tokens/sinks.ts +84 -0
  206. package/src/ui/flow.ts +451 -0
  207. package/src/ui/react.ts +112 -0
  208. package/src/ui/routeAllowlist.ts +208 -0
  209. package/src/ui/scaffold.ts +113 -0
  210. package/src/ui/screenStyles.ts +127 -0
  211. package/src/ui/stubs.ts +135 -0
  212. package/src/ui/templates.ts +52 -0
  213. package/src/ui/wire.ts +311 -0
  214. package/src/ui/workerUi.ts +172 -0
  215. package/templates/starter/.dev.secrets.example.jsonc +43 -0
  216. package/templates/starter/.dev.vars.example +30 -0
  217. package/templates/starter/apps/api/package.json +22 -0
  218. package/templates/starter/apps/api/pithy.config.ts +65 -0
  219. package/templates/starter/apps/api/pithy.worker.jsonc +11 -0
  220. package/templates/starter/apps/api/src/bindings.workers.test.ts +18 -0
  221. package/templates/starter/apps/api/src/cloudflare-test.d.ts +11 -0
  222. package/templates/starter/apps/api/src/index.ts +8 -0
  223. package/templates/starter/apps/api/tsconfig.json +26 -0
  224. package/templates/starter/apps/api/wrangler.jsonc +68 -0
  225. package/templates/starter/biome.template.jsonc +75 -0
  226. package/templates/starter/gitignore +37 -0
  227. package/templates/starter/package.json +28 -0
  228. package/templates/starter/pithy.config.ts +67 -0
  229. package/templates/starter/plugins/no-console.grit +25 -0
  230. package/templates/starter/plugins/no-process-io.grit +25 -0
  231. package/templates/starter/tsconfig.json +14 -0
  232. package/templates/starter/tsconfig.tools.json +30 -0
  233. package/templates/starter/vitest.config.ts +124 -0
  234. 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
+ }