@canonical/pragma-cli 0.36.0 → 0.38.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/README.md +3 -2
- package/bundled-skills/adoption-a1-styles/SKILL.md +24 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +22 -6
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +1397 -327
- package/bundled-skills/anatomy-author/SKILL.md +840 -137
- package/bundled-skills/design-auditor/SKILL.md +133 -9
- package/bundled-skills/implement-component/SKILL.md +192 -23
- package/bundled-skills/specify-component/SKILL.md +124 -12
- package/bundled-skills/specify-pattern/SKILL.md +47 -8
- package/dist/package.json +23 -20
- package/dist/pragma.conf.js +963 -40
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/capabilities/capabilities/catalog.js +14 -5
- package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
- package/dist/src/capabilities/capabilities/hints.js +33 -5
- package/dist/src/capabilities/capabilities/hints.js.map +1 -1
- package/dist/src/capabilities/config/show.render.js +30 -2
- package/dist/src/capabilities/config/show.render.js.map +1 -1
- package/dist/src/capabilities/create/create.verb.js +2 -1
- package/dist/src/capabilities/create/create.verb.js.map +1 -1
- package/dist/src/capabilities/create/createSurface.generated.js +23 -1
- package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/targetHealth.js +104 -14
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/graph/connect.render.js +101 -0
- package/dist/src/capabilities/graph/connect.render.js.map +1 -0
- package/dist/src/capabilities/graph/connect.types.js +21 -0
- package/dist/src/capabilities/graph/connect.types.js.map +1 -0
- package/dist/src/capabilities/graph/connect.verb.js +147 -0
- package/dist/src/capabilities/graph/connect.verb.js.map +1 -0
- package/dist/src/capabilities/graph/index.js +5 -3
- package/dist/src/capabilities/graph/index.js.map +1 -1
- package/dist/src/capabilities/graph/queryText.js +206 -0
- package/dist/src/capabilities/graph/queryText.js.map +1 -0
- package/dist/src/capabilities/graph/relationIndex.js +302 -0
- package/dist/src/capabilities/graph/relationIndex.js.map +1 -0
- package/dist/src/capabilities/graph/runConnect.js +456 -0
- package/dist/src/capabilities/graph/runConnect.js.map +1 -0
- package/dist/src/capabilities/graph/runQuery.js +21 -4
- package/dist/src/capabilities/graph/runQuery.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +6 -2
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/operations/index.js +7 -2
- package/dist/src/capabilities/setup/operations/index.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
- package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
- package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +72 -14
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +372 -62
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +150 -5
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +4 -2
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/operations/writability.js +114 -0
- package/dist/src/capabilities/setup/operations/writability.js.map +1 -0
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +80 -31
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +124 -42
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +1 -1
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/constants.js +7 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/error/renderError.js +3 -2
- package/dist/src/kernel/error/renderError.js.map +1 -1
- package/dist/src/kernel/error/validOptions.js +41 -0
- package/dist/src/kernel/error/validOptions.js.map +1 -0
- package/dist/src/kernel/packs/compile.js +90 -6
- package/dist/src/kernel/packs/compile.js.map +1 -1
- package/dist/src/kernel/packs/cursor.js +135 -0
- package/dist/src/kernel/packs/cursor.js.map +1 -0
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js +9 -2
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js.map +1 -1
- package/dist/src/kernel/packs/paging.js +108 -0
- package/dist/src/kernel/packs/paging.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +154 -5
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +118 -19
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +156 -18
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +160 -12
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/authorQuery.js +227 -0
- package/dist/src/kernel/packs/sparql/authorQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js +251 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +125 -22
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/sparql/filterValues.js +145 -0
- package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -0
- package/dist/src/kernel/packs/sparql/index.js +24 -14
- package/dist/src/kernel/packs/sparql/index.js.map +1 -1
- package/dist/src/kernel/packs/sparql/runSelect.js +10 -4
- package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
- package/dist/src/kernel/packs/sparql/searchTerm.js +25 -0
- package/dist/src/kernel/packs/sparql/searchTerm.js.map +1 -0
- package/dist/src/kernel/packs/storyRules.js +151 -0
- package/dist/src/kernel/packs/storyRules.js.map +1 -0
- package/dist/src/kernel/packs/tierScope.js +310 -0
- package/dist/src/kernel/packs/tierScope.js.map +1 -0
- package/dist/src/kernel/packs/types.js +80 -0
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +10 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +104 -3
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +8 -5
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/cli/suggestNames.js +31 -7
- package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +5 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +34 -2
- package/dist/src/kernel/render/renderers.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
- package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
- package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/read.js +17 -10
- package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
- package/dist/src/kernel/runtime/loadSession.js +3 -1
- package/dist/src/kernel/runtime/loadSession.js.map +1 -1
- package/dist/src/kernel/runtime/probe.js +1 -1
- package/dist/src/kernel/runtime/probe.js.map +1 -1
- package/dist/src/kernel/runtime/readEntity.js +19 -4
- package/dist/src/kernel/runtime/readEntity.js.map +1 -1
- package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
- package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
- package/dist/src/kernel/runtime/refs/resolve.js +38 -5
- package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +23 -20
- package/pragma.conf.ts +983 -40
- package/src/capabilities/block.parity.test.ts +207 -27
- package/src/capabilities/block.shipped.exec.test.ts +156 -14
- package/src/capabilities/block.tierRank.exec.test.ts +46 -9
- package/src/capabilities/capabilities/catalog.ts +14 -5
- package/src/capabilities/capabilities/hints.ts +40 -5
- package/src/capabilities/config/collectConfigShow.test.ts +1 -0
- package/src/capabilities/config/set.test.ts +24 -25
- package/src/capabilities/config/show.render.test.ts +9 -4
- package/src/capabilities/config/show.render.ts +30 -2
- package/src/capabilities/create/byteEquality.test.ts +28 -1
- package/src/capabilities/create/create.test.ts +1 -0
- package/src/capabilities/create/create.verb.ts +2 -1
- package/src/capabilities/create/createGrammar.test.ts +4 -1
- package/src/capabilities/create/createSurface.generated.ts +23 -1
- package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
- package/src/capabilities/distribution.parity.test.ts +12 -11
- package/src/capabilities/distribution.test.ts +77 -7
- package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +13 -6
- package/src/capabilities/doctor/checks/harnessInventory.ts +8 -4
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
- package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
- package/src/capabilities/doctor/checks/targetHealth.ts +114 -13
- package/src/capabilities/doctor/doctor.test.ts +327 -36
- package/src/capabilities/emptyRecovery.test.ts +84 -28
- package/src/capabilities/graph/connect.render.ts +141 -0
- package/src/capabilities/graph/connect.shipped.exec.test.ts +370 -0
- package/src/capabilities/graph/connect.test.ts +361 -0
- package/src/capabilities/graph/connect.types.ts +136 -0
- package/src/capabilities/graph/connect.verb.ts +165 -0
- package/src/capabilities/graph/index.ts +5 -3
- package/src/capabilities/graph/query.shipped.exec.test.ts +66 -0
- package/src/capabilities/graph/query.test.ts +17 -0
- package/src/capabilities/graph/queryText.test.ts +121 -0
- package/src/capabilities/graph/queryText.ts +240 -0
- package/src/capabilities/graph/relationIndex.ts +372 -0
- package/src/capabilities/graph/runConnect.ts +577 -0
- package/src/capabilities/graph/runQuery.ts +21 -6
- package/src/capabilities/lazy.test.ts +32 -2
- package/src/capabilities/listBudget.shipped.exec.test.ts +239 -0
- package/src/capabilities/listLookup.shipped.exec.test.ts +387 -0
- package/src/capabilities/listQuery.shipped.exec.test.ts +400 -0
- package/src/capabilities/resources/resources.test.ts +38 -2
- package/src/capabilities/setup/buildPlan.ts +7 -1
- package/src/capabilities/setup/operations/index.ts +14 -0
- package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
- package/src/capabilities/setup/operations/setupConfig.ts +7 -2
- package/src/capabilities/setup/operations/setupGenerator.ts +93 -15
- package/src/capabilities/setup/operations/setupLsp.ts +544 -85
- package/src/capabilities/setup/operations/setupMcp.ts +212 -4
- package/src/capabilities/setup/operations/setupSkills.ts +7 -2
- package/src/capabilities/setup/operations/writability.test.ts +151 -0
- package/src/capabilities/setup/operations/writability.ts +135 -0
- package/src/capabilities/setup/plan.ts +11 -0
- package/src/capabilities/setup/setup.test.ts +992 -16
- package/src/capabilities/setup/setup.verb.ts +84 -36
- package/src/capabilities/setup/targets.ts +152 -48
- package/src/capabilities/setup/types.ts +41 -5
- package/src/capabilities/shared/index.ts +1 -0
- package/src/capabilities/sources/sources.test.ts +55 -0
- package/src/capabilities/surface.test.ts +59 -11
- package/src/capabilities/tier.parity.test.ts +2 -2
- package/src/capabilities/token.parity.test.ts +784 -0
- package/src/capabilities/tokenGraph.cli.test.ts +279 -0
- package/src/capabilities/tokenGraph.shipped.exec.test.ts +653 -0
- package/src/capabilities/variable.parity.test.ts +448 -0
- package/src/constants.ts +8 -0
- package/src/kernel/completion/shellDrive.test.ts +32 -30
- package/src/kernel/config/defaults.test.ts +21 -2
- package/src/kernel/config/readConfig.test.ts +1 -1
- package/src/kernel/error/renderError.ts +3 -2
- package/src/kernel/error/validOptions.test.ts +57 -0
- package/src/kernel/error/validOptions.ts +45 -0
- package/src/kernel/packs/collect.test.ts +2 -0
- package/src/kernel/packs/compile.test.ts +188 -15
- package/src/kernel/packs/compile.ts +109 -14
- package/src/kernel/packs/cursor.ts +135 -0
- package/src/kernel/packs/filteredEmpty.test.ts +213 -0
- package/src/kernel/packs/graphql/buildLookupDocument.ts +16 -9
- package/src/kernel/packs/graphqlSource.test.ts +152 -0
- package/src/kernel/packs/listNotice.test.ts +156 -0
- package/src/kernel/packs/paging.ts +110 -0
- package/src/kernel/packs/renderPack.ts +186 -13
- package/src/kernel/packs/resolveEntity.test.ts +62 -3
- package/src/kernel/packs/resolveEntity.ts +186 -26
- package/src/kernel/packs/runBodies.ts +196 -26
- package/src/kernel/packs/schema.ts +180 -13
- package/src/kernel/packs/sparql/authorQuery.test.ts +179 -0
- package/src/kernel/packs/sparql/authorQuery.ts +257 -0
- package/src/kernel/packs/sparql/buildListQuery.test.ts +344 -0
- package/src/kernel/packs/sparql/buildListQuery.ts +344 -0
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +138 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +140 -20
- package/src/kernel/packs/sparql/filterValues.test.ts +86 -0
- package/src/kernel/packs/sparql/filterValues.ts +182 -0
- package/src/kernel/packs/sparql/index.ts +30 -14
- package/src/kernel/packs/sparql/runSelect.test.ts +63 -0
- package/src/kernel/packs/sparql/runSelect.ts +42 -2
- package/src/kernel/packs/sparql/searchTerm.ts +29 -0
- package/src/kernel/packs/storyRules.ts +180 -0
- package/src/kernel/packs/tierScope.test.ts +487 -0
- package/src/kernel/packs/tierScope.ts +409 -0
- package/src/kernel/packs/toolDescription.test.ts +13 -10
- package/src/kernel/packs/types.ts +311 -9
- package/src/kernel/project/cli/buildProgram.ts +17 -1
- package/src/kernel/project/cli/dispatch.test.ts +87 -0
- package/src/kernel/project/cli/dispatch.ts +114 -4
- package/src/kernel/project/cli/rootHelp.ts +8 -5
- package/src/kernel/project/cli/suggest.test.ts +35 -0
- package/src/kernel/project/cli/suggestNames.ts +34 -7
- package/src/kernel/project/mcp/registerVerb.ts +5 -1
- package/src/kernel/render/contracts.ts +2 -0
- package/src/kernel/render/prefixes.test.ts +2 -0
- package/src/kernel/render/renderers.test.ts +43 -0
- package/src/kernel/render/renderers.ts +34 -1
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
- package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +14 -6
- package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
- package/src/kernel/runtime/graphpack/read.ts +17 -13
- package/src/kernel/runtime/loadSession.ts +3 -1
- package/src/kernel/runtime/probe.ts +1 -1
- package/src/kernel/runtime/readEntity.ts +19 -4
- package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
- package/src/kernel/runtime/refs/parseRef.ts +25 -1
- package/src/kernel/runtime/refs/resolve.ts +39 -5
- package/src/kernel/runtime/types.ts +30 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/types.ts +17 -0
- package/src/testing/PARITY_GAPS.ts +4 -4
- package/src/testing/behavioral/agentSession.mcp.test.ts +43 -39
- package/src/testing/behavioral/completion.test.ts +7 -6
- package/src/testing/behavioral/disclosure.test.ts +39 -37
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +82 -64
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +11 -10
- package/src/testing/behavioral/journeys.cli.test.ts +71 -32
- package/src/testing/behavioral/journeys.defaultPack.test.ts +6 -4
- package/src/testing/behavioral/journeys.firstInstall.test.ts +70 -5
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +4 -3
- package/src/testing/behavioral/journeys.packageStories.test.ts +2 -1
- package/src/testing/behavioral/mcpSurface.test.ts +28 -25
- package/src/testing/behavioral/parity.test.ts +25 -23
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +20 -0
- package/src/testing/eval/cases/effectful.ts +7 -1
- package/src/testing/eval/cases/readNouns.ts +100 -11
- package/src/testing/fixtures/blockGraph.ts +89 -5
- package/src/testing/fixtures/connectGraph.ts +159 -0
- package/src/testing/fixtures/graph/canonical.ts +128 -32
- package/src/testing/helpers/fsProbe.ts +88 -0
- package/src/testing/helpers/runCli.test.ts +44 -0
- package/src/testing/helpers/runCli.ts +27 -3
- package/src/testing/perf/budgets.test.ts +3 -1
- package/src/testing/perf/budgets.ts +138 -54
- package/dist/src/kernel/packs/sparql/applyFilters.js +0 -167
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +0 -1
- package/dist/src/kernel/packs/sparql/applySearch.js +0 -25
- package/dist/src/kernel/packs/sparql/applySearch.js.map +0 -1
- package/src/kernel/packs/sparql/applyFilters.ts +0 -203
- package/src/kernel/packs/sparql/applySearch.ts +0 -33
|
@@ -11,18 +11,39 @@
|
|
|
11
11
|
* NOT used: it hardcodes the `code` CLI, which is precisely what left
|
|
12
12
|
* VSCodium/Cursor/Windsurf/Antigravity machines unable to install at all.
|
|
13
13
|
*
|
|
14
|
-
* `detectLsp` probes FOR REAL up front, and spawns NOTHING
|
|
15
|
-
* detected
|
|
16
|
-
*
|
|
14
|
+
* `detectLsp` probes FOR REAL up front, and spawns NOTHING. An editor is
|
|
15
|
+
* detected THREE ways, and each row records which one found it, because "no
|
|
16
|
+
* editor found" and "found but nothing can be installed into it" are different
|
|
17
|
+
* answers and only one of them is about this machine lacking the editor:
|
|
18
|
+
*
|
|
19
|
+
* 1. its CLI on PATH (the `editorClis` registry names them);
|
|
20
|
+
* 2. its CLI inside its macOS APP BUNDLE — a stock macOS install puts nothing
|
|
21
|
+
* on PATH until the user runs the palette's "Shell Command: Install
|
|
22
|
+
* '<cli>' command in PATH", which is opt-in and usually never taken, so a
|
|
23
|
+
* PATH-only probe reported a machine that HAS VS Code as having none;
|
|
24
|
+
* 3. its per-user configuration DIRECTORY, which is the last trace a fresh
|
|
25
|
+
* install leaves. Nothing can be installed through a directory, so such an
|
|
26
|
+
* editor carries no CLI path at all and its remedy is the palette command
|
|
27
|
+
* that settles it.
|
|
28
|
+
*
|
|
29
|
+
* It carries the extension when its extensions dir holds a
|
|
17
30
|
* `canonical.terrazzo-lsp-extension-<version>/` entry, and is "installed" when
|
|
18
|
-
* that entry is also new enough to work. Running
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
31
|
+
* that entry is also new enough to work. Running `--list-extensions` instead
|
|
32
|
+
* would be the natural probe, but Cursor's Linux launcher OPENS THE EDITOR on
|
|
33
|
+
* that flag — a detection step must never launch an app, so the fs is the
|
|
34
|
+
* source of truth. The full extension id is matched (not the old `terrazzo`
|
|
35
|
+
* substring, which any other terrazzo-named extension could false-positive).
|
|
36
|
+
*
|
|
37
|
+
* Detection also asks whether the extensions directory could be WRITTEN, via
|
|
38
|
+
* `probeWritable`, BEFORE any install is composed. A home-manager-managed
|
|
39
|
+
* `~/.vscode/extensions` is a symlink into `/nix/store`, so
|
|
40
|
+
* `--install-extension` there failed with a raw fs error or silently changed
|
|
41
|
+
* nothing; that is now a named skip carrying the declaration that does work.
|
|
42
|
+
* Per the owner, 2026-09-16, the decision is made by the DIRECTORY, never by
|
|
43
|
+
* the OS: a Nix user's home is ordinary, and a non-Nix user can have a
|
|
44
|
+
* read-only mount.
|
|
45
|
+
*
|
|
46
|
+
* Every one of those is a NAMED SKIP, not an error: the message says what was
|
|
26
47
|
* looked for and what to do, and the run exits 0 — there is nothing on this
|
|
27
48
|
* machine the step could honestly fail at. Real failures (fetch, sideload)
|
|
28
49
|
* travel the task failure channel (`checkExecOk`) with a recovery that names a
|
|
@@ -36,24 +57,81 @@ import type { EditorCliDefinition, PlatformEnv } from "@canonical/harnesses";
|
|
|
36
57
|
import {
|
|
37
58
|
type ExecResult,
|
|
38
59
|
exec,
|
|
60
|
+
exists,
|
|
39
61
|
flatMap,
|
|
40
62
|
mkdir,
|
|
63
|
+
pure,
|
|
64
|
+
sequence,
|
|
41
65
|
sequence_,
|
|
42
66
|
type Task,
|
|
43
67
|
} from "@canonical/task";
|
|
44
68
|
import { BIN_NAME } from "../../../constants.js";
|
|
45
|
-
import {
|
|
46
|
-
import
|
|
69
|
+
import { PragmaError } from "../../../kernel/error/index.js";
|
|
70
|
+
import {
|
|
71
|
+
checkExecOk,
|
|
72
|
+
failPragma,
|
|
73
|
+
guardMissingBinary,
|
|
74
|
+
} from "../../shared/index.js";
|
|
75
|
+
import { type Roots, shortenPath } from "../plan.js";
|
|
76
|
+
import type { LspState, WriteBlock } from "../types.js";
|
|
77
|
+
import { type FsProbe, probeWritable } from "./writability.js";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The extension's publisher and name. Separate constants because the Nix
|
|
81
|
+
* declaration names all three parts (`vscodeExtPublisher`, `vscodeExtName`,
|
|
82
|
+
* `vscodeExtUniqueId`), and composing the id from them is what keeps the three
|
|
83
|
+
* from drifting apart in one string.
|
|
84
|
+
*/
|
|
85
|
+
const EXTENSION_PUBLISHER = "canonical";
|
|
86
|
+
const EXTENSION_NAME = "terrazzo-lsp-extension";
|
|
47
87
|
|
|
48
88
|
/** The full extension id, as the editor's extensions dir spells it. */
|
|
49
|
-
const EXTENSION_ID =
|
|
89
|
+
const EXTENSION_ID = `${EXTENSION_PUBLISHER}.${EXTENSION_NAME}`;
|
|
50
90
|
|
|
51
91
|
/** The npm package whose tarball bundles the VSIX. */
|
|
52
92
|
const LSP_PACKAGE = "@canonical/terrazzo-lsp-extension";
|
|
53
93
|
|
|
54
94
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
95
|
+
* What a probe found, and the evidence that travels with it — a UNION, because
|
|
96
|
+
* the three probes do not produce the same thing.
|
|
97
|
+
*
|
|
98
|
+
* A CLI probe yields a file to run; the user-directory probe yields only the
|
|
99
|
+
* knowledge that the editor is here. Making `cliPath` optional on one flat
|
|
100
|
+
* record said instead that any editor MIGHT have one, which put a
|
|
101
|
+
* `cliPath ?? editor.cli` fallback at every exec site — four of them, none
|
|
102
|
+
* reachable, each quietly ready to spawn a bare name on a machine where the
|
|
103
|
+
* probe had already proved there is none.
|
|
104
|
+
*/
|
|
105
|
+
type EditorSource =
|
|
106
|
+
| {
|
|
107
|
+
/**
|
|
108
|
+
* WHICH CLI probe found it. Reported per editor by both surfaces,
|
|
109
|
+
* because it is the fact that tells a user whose `setup lsp` did nothing
|
|
110
|
+
* whether the editor was missed or merely unreachable — the question the
|
|
111
|
+
* old single-line "no editor CLI on PATH" could not answer.
|
|
112
|
+
*/
|
|
113
|
+
readonly foundBy: "path" | "bundle";
|
|
114
|
+
/**
|
|
115
|
+
* The CLI file the probe actually found. Every exec runs THIS path
|
|
116
|
+
* rather than the bare name: a bundle CLI is not on PATH at all, and
|
|
117
|
+
* even for a PATH hit the probed file is the one this run verified
|
|
118
|
+
* exists — re-resolving the bare name at spawn time could reach another.
|
|
119
|
+
*/
|
|
120
|
+
readonly cliPath: string;
|
|
121
|
+
}
|
|
122
|
+
| {
|
|
123
|
+
/**
|
|
124
|
+
* Found by its per-user configuration DIRECTORY and nothing else. There
|
|
125
|
+
* is no CLI to run, so this editor is never what the installer runs for;
|
|
126
|
+
* the palette command that creates one is its remedy.
|
|
127
|
+
*/
|
|
128
|
+
readonly foundBy: "user-dir";
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* What every detected editor carries, whichever probe found it: its registry
|
|
133
|
+
* row, its extensions directory, and the two DIFFERENT questions about the
|
|
134
|
+
* extension.
|
|
57
135
|
*
|
|
58
136
|
* `present` and `installed` are deliberately separate, and collapsing them is a
|
|
59
137
|
* silent wrong-removal. `installed` is version-gated — an entry older than
|
|
@@ -64,7 +142,7 @@ const LSP_PACKAGE = "@canonical/terrazzo-lsp-extension";
|
|
|
64
142
|
* So removal keys on `present` (any matching entry at all) and everything else
|
|
65
143
|
* keys on `installed` (present AND new enough to work).
|
|
66
144
|
*/
|
|
67
|
-
|
|
145
|
+
interface EditorFacts {
|
|
68
146
|
readonly editor: EditorCliDefinition;
|
|
69
147
|
/** Any `canonical.terrazzo-lsp-extension-*` entry, whatever its version. */
|
|
70
148
|
readonly present: boolean;
|
|
@@ -76,14 +154,75 @@ export interface DetectedEditor {
|
|
|
76
154
|
* {@link composeLspRemoval}); when `present` is true, this is that path.
|
|
77
155
|
*/
|
|
78
156
|
readonly extensionsDir: string;
|
|
157
|
+
/**
|
|
158
|
+
* The matching entry NAMES under {@link extensionsDir} — the artifacts
|
|
159
|
+
* `present` is true about. The removal's postcondition probes exactly
|
|
160
|
+
* these after the uninstall (see {@link composeLspRemoval}): the editor
|
|
161
|
+
* CLI's exit code and the directory listing are DIFFERENT success
|
|
162
|
+
* criteria, and only the directory is the one detection (and doctor)
|
|
163
|
+
* believe.
|
|
164
|
+
*/
|
|
165
|
+
readonly presentEntries: readonly string[];
|
|
166
|
+
/**
|
|
167
|
+
* Why this editor's extensions directory must not be written, when that is
|
|
168
|
+
* the case. Absent means the folder is fine — which is not the same as "the
|
|
169
|
+
* install can proceed": an editor found only by its user directory has no
|
|
170
|
+
* CLI to run whatever its folder permits (see {@link installable}).
|
|
171
|
+
*
|
|
172
|
+
* A block is carried on the DETECTION rather than discovered by the writer
|
|
173
|
+
* on purpose. The write is what used to discover it, and it discovered it as
|
|
174
|
+
* a raw `EACCES` on a `failed` row with no next step; probing first turns
|
|
175
|
+
* the same machine into a named skip with a remedy, and lets doctor report it
|
|
176
|
+
* without writing anything at all.
|
|
177
|
+
*/
|
|
178
|
+
readonly block?: WriteBlock;
|
|
79
179
|
}
|
|
80
180
|
|
|
181
|
+
/** One detected editor: the facts, plus what found it. */
|
|
182
|
+
export type DetectedEditor = EditorFacts & EditorSource;
|
|
183
|
+
|
|
184
|
+
/** An editor with a CLI to run — what {@link installable} narrows to. */
|
|
185
|
+
type RunnableEditor = EditorFacts & Extract<EditorSource, { cliPath: string }>;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* An editor nothing can be installed into: no CLI to run at all, or a folder
|
|
189
|
+
* this command must not write to. The two shapes {@link describeBlock} answers
|
|
190
|
+
* for, and the type that lets it read `block` without re-asking whether it is
|
|
191
|
+
* there.
|
|
192
|
+
*/
|
|
193
|
+
type BlockedEditor =
|
|
194
|
+
| (EditorFacts & Extract<EditorSource, { foundBy: "user-dir" }>)
|
|
195
|
+
| (EditorFacts & {
|
|
196
|
+
readonly foundBy: "path" | "bundle";
|
|
197
|
+
readonly cliPath: string;
|
|
198
|
+
readonly block: WriteBlock;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Whether an install can act on an editor at all: there is a CLI to RUN, and
|
|
203
|
+
* its extensions folder can be WRITTEN.
|
|
204
|
+
*
|
|
205
|
+
* One predicate, because the answer is one question. Five call sites used to
|
|
206
|
+
* spell their own half of it (`!e.installed && e.block === undefined`,
|
|
207
|
+
* `e.present && e.block === undefined`, …), so adding a third way to be
|
|
208
|
+
* unreachable would have meant finding all five.
|
|
209
|
+
*
|
|
210
|
+
* @param e - One detected editor.
|
|
211
|
+
* @returns Whether the sideload can run for it.
|
|
212
|
+
* @note Pure — reads the detection record.
|
|
213
|
+
*/
|
|
214
|
+
const installable = (e: DetectedEditor): e is RunnableEditor =>
|
|
215
|
+
e.foundBy !== "user-dir" && e.block === undefined;
|
|
216
|
+
|
|
217
|
+
/** The editors nothing can be installed into, in registry order. */
|
|
218
|
+
const blockedEditors = (d: LspDetection): readonly BlockedEditor[] =>
|
|
219
|
+
d.editors.filter((e): e is BlockedEditor => !installable(e));
|
|
220
|
+
|
|
81
221
|
/**
|
|
82
|
-
* The detected LSP state:
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* the extension, `absent` otherwise (the installer runs for the missing ones).
|
|
222
|
+
* The detected LSP state: every editor the three probes found (each with its
|
|
223
|
+
* provenance, its CLI file when there is one, its installed-state and any
|
|
224
|
+
* block on its extensions folder), the staging dir the VSIX is fetched into,
|
|
225
|
+
* and the aggregate {@link LspState}.
|
|
87
226
|
*
|
|
88
227
|
* `probed` is every CLI name that was LOOKED for, kept so the skip row can name
|
|
89
228
|
* them: a skip that says only "no editor found" leaves the user guessing what
|
|
@@ -98,8 +237,9 @@ export interface LspDetection {
|
|
|
98
237
|
}
|
|
99
238
|
|
|
100
239
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
240
|
+
* The FIRST candidate path that exists, or `undefined` when none does — used
|
|
241
|
+
* for both candidate lists (PATH, then the macOS app bundle), so a caller
|
|
242
|
+
* learns not only whether the CLI is there but which file it is.
|
|
103
243
|
*
|
|
104
244
|
* The candidates come from `@canonical/harnesses`' `executableCandidates` —
|
|
105
245
|
* the same helper harness detection resolves `process` signals with — rather
|
|
@@ -111,9 +251,13 @@ export interface LspDetection {
|
|
|
111
251
|
* the editor — a wrong skip reads as correct, which is worse than a wrong
|
|
112
252
|
* failure. One implementation of the platform rules means the probe cannot
|
|
113
253
|
* drift away from detection's.
|
|
254
|
+
*
|
|
255
|
+
* @param candidates - The candidate paths, strongest first.
|
|
256
|
+
* @returns The first candidate that exists, or `undefined` when none does.
|
|
257
|
+
* @note Impure — stats each candidate until one exists.
|
|
114
258
|
*/
|
|
115
|
-
const
|
|
116
|
-
candidates.
|
|
259
|
+
const resolveCli = (candidates: readonly string[]): string | undefined =>
|
|
260
|
+
candidates.find((candidate) => existsSync(candidate));
|
|
117
261
|
|
|
118
262
|
/**
|
|
119
263
|
* The oldest extension release whose bundled language server actually starts.
|
|
@@ -187,12 +331,13 @@ const compareVersions = (left: number[], right: number[]): number => {
|
|
|
187
331
|
const extensionState = (
|
|
188
332
|
editor: EditorCliDefinition,
|
|
189
333
|
platform: PlatformEnv,
|
|
190
|
-
): { present: boolean; current: boolean } => {
|
|
334
|
+
): { present: boolean; current: boolean; entries: string[] } => {
|
|
191
335
|
let entries: string[];
|
|
192
336
|
try {
|
|
193
337
|
entries = readdirSync(editor.extensionsDir(platform));
|
|
194
338
|
} catch {
|
|
195
|
-
|
|
339
|
+
// No dir — nothing installed.
|
|
340
|
+
return { present: false, current: false, entries: [] };
|
|
196
341
|
}
|
|
197
342
|
const prefix = `${EXTENSION_ID}-`;
|
|
198
343
|
const matching = entries.filter((entry) =>
|
|
@@ -203,32 +348,138 @@ const extensionState = (
|
|
|
203
348
|
if (version === undefined) return false; // Unreadable: cannot vouch for it.
|
|
204
349
|
return compareVersions(version, MINIMUM) >= 0;
|
|
205
350
|
});
|
|
206
|
-
return { present: matching.length > 0, current };
|
|
351
|
+
return { present: matching.length > 0, current, entries: matching };
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The path helpers the editor probe resolves candidates with, captured from
|
|
356
|
+
* the dynamic `@canonical/harnesses` import that keeps the package off the
|
|
357
|
+
* fast path (so they cannot be static imports here).
|
|
358
|
+
*/
|
|
359
|
+
type EditorPaths = Pick<
|
|
360
|
+
typeof import("@canonical/harnesses"),
|
|
361
|
+
"appBundleCandidates" | "executableCandidates" | "vscodeUserDir"
|
|
362
|
+
>;
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* WHICH of the three probes finds this editor, strongest first — or
|
|
366
|
+
* `undefined` when none does.
|
|
367
|
+
*
|
|
368
|
+
* A resolved CLI always wins, because a CLI is what an install RUNS. The
|
|
369
|
+
* per-user directory is last and yields no path: it proves the editor is on
|
|
370
|
+
* this machine and nothing more.
|
|
371
|
+
*
|
|
372
|
+
* @param editor - One registry row.
|
|
373
|
+
* @param host - The captured platform.
|
|
374
|
+
* @param paths - The harness path helpers.
|
|
375
|
+
* @returns The provenance and its evidence, or `undefined`.
|
|
376
|
+
* @note Impure — stats PATH entries, app bundles and the user directory.
|
|
377
|
+
*/
|
|
378
|
+
const findEditorSource = (
|
|
379
|
+
editor: EditorCliDefinition,
|
|
380
|
+
host: PlatformEnv,
|
|
381
|
+
paths: EditorPaths,
|
|
382
|
+
): EditorSource | undefined => {
|
|
383
|
+
const onPath = resolveCli(paths.executableCandidates(editor.cli, host));
|
|
384
|
+
if (onPath !== undefined) return { foundBy: "path", cliPath: onPath };
|
|
385
|
+
const inBundle = resolveCli(
|
|
386
|
+
paths.appBundleCandidates(editor.cli, host, editor.darwinBundles),
|
|
387
|
+
);
|
|
388
|
+
if (inBundle !== undefined) return { foundBy: "bundle", cliPath: inBundle };
|
|
389
|
+
// The registry names the PRODUCT; `vscodeUserDir` owns the platform switch,
|
|
390
|
+
// so this probe, the harness rows' `homeConfigPath` and their detection
|
|
391
|
+
// signals cannot name different directories.
|
|
392
|
+
const userDir =
|
|
393
|
+
editor.product === undefined
|
|
394
|
+
? undefined
|
|
395
|
+
: paths.vscodeUserDir(editor.product, host);
|
|
396
|
+
if (userDir !== undefined && existsSync(userDir)) {
|
|
397
|
+
return { foundBy: "user-dir" };
|
|
398
|
+
}
|
|
399
|
+
return undefined;
|
|
207
400
|
};
|
|
208
401
|
|
|
209
402
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
403
|
+
* One registry row as a {@link DetectedEditor} — or `undefined` when this
|
|
404
|
+
* machine has no trace of the editor at all.
|
|
405
|
+
*
|
|
406
|
+
* @param editor - One registry row.
|
|
407
|
+
* @param host - The captured platform.
|
|
408
|
+
* @param paths - The harness path helpers.
|
|
409
|
+
* @param probe - The writability filesystem seam; defaults to the real one.
|
|
410
|
+
* @returns The detection record, or `undefined`.
|
|
411
|
+
* @note Impure — reads PATH dirs, app bundles, the user directory and the
|
|
412
|
+
* extensions directory off the real fs.
|
|
413
|
+
*/
|
|
414
|
+
const findEditor = (
|
|
415
|
+
editor: EditorCliDefinition,
|
|
416
|
+
host: PlatformEnv,
|
|
417
|
+
paths: EditorPaths,
|
|
418
|
+
probe?: FsProbe,
|
|
419
|
+
): DetectedEditor | undefined => {
|
|
420
|
+
const source = findEditorSource(editor, host, paths);
|
|
421
|
+
if (source === undefined) return undefined;
|
|
422
|
+
const { present, current, entries } = extensionState(editor, host);
|
|
423
|
+
const extensionsDir = editor.extensionsDir(host);
|
|
424
|
+
// Writability is asked only where there is a CLI to run. An editor found
|
|
425
|
+
// only by its user directory cannot be installed into whatever its folder
|
|
426
|
+
// permits, and the palette command is its remedy either way — so the probe
|
|
427
|
+
// would cost a stat to answer a question nothing reads.
|
|
428
|
+
const block =
|
|
429
|
+
source.foundBy === "user-dir"
|
|
430
|
+
? undefined
|
|
431
|
+
: probeWritable(extensionsDir, probe);
|
|
432
|
+
return {
|
|
433
|
+
editor,
|
|
434
|
+
...source,
|
|
435
|
+
present,
|
|
436
|
+
installed: current,
|
|
437
|
+
extensionsDir,
|
|
438
|
+
presentEntries: entries,
|
|
439
|
+
...(block === undefined ? {} : { block }),
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Probe every registry editor: its CLI on PATH, then inside its macOS app
|
|
445
|
+
* bundle, then its per-user directory; its installed-state via the extensions
|
|
446
|
+
* dir; and whether that dir could be written. No spawns (see the module
|
|
447
|
+
* docblock for why).
|
|
212
448
|
*
|
|
213
449
|
* @param _cwd - Unused; kept so every `detectX` shares the (cwd) shape.
|
|
450
|
+
* @param platform - The captured host; defaults to the live one. Injected so a
|
|
451
|
+
* test can drive the darwin arm — the one this repo's CI host cannot run.
|
|
452
|
+
* @param probe - The writability filesystem seam; defaults to the real one.
|
|
214
453
|
* @returns The detected {@link LspDetection}.
|
|
215
|
-
* @note Impure — reads PATH dirs and editor extension dirs off
|
|
454
|
+
* @note Impure — reads PATH dirs, app bundles and editor extension dirs off
|
|
455
|
+
* the real fs.
|
|
216
456
|
*/
|
|
217
|
-
export async function detectLsp(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
457
|
+
export async function detectLsp(
|
|
458
|
+
_cwd: string,
|
|
459
|
+
platform?: PlatformEnv,
|
|
460
|
+
probe?: FsProbe,
|
|
461
|
+
): Promise<LspDetection> {
|
|
462
|
+
const {
|
|
463
|
+
appBundleCandidates,
|
|
464
|
+
editorClis,
|
|
465
|
+
executableCandidates,
|
|
466
|
+
readPlatformEnv,
|
|
467
|
+
userDataBase,
|
|
468
|
+
vscodeUserDir,
|
|
469
|
+
} = await import("@canonical/harnesses");
|
|
470
|
+
// Not a default parameter value: `readPlatformEnv` arrives through the
|
|
471
|
+
// dynamic import that keeps `@canonical/harnesses` off the fast path, so it
|
|
472
|
+
// cannot be named in the signature.
|
|
473
|
+
const host = platform ?? readPlatformEnv();
|
|
474
|
+
const paths: EditorPaths = {
|
|
475
|
+
appBundleCandidates,
|
|
476
|
+
executableCandidates,
|
|
477
|
+
vscodeUserDir,
|
|
478
|
+
};
|
|
479
|
+
const editors: DetectedEditor[] = editorClis.flatMap((editor) => {
|
|
480
|
+
const found = findEditor(editor, host, paths, probe);
|
|
481
|
+
return found === undefined ? [] : [found];
|
|
482
|
+
});
|
|
232
483
|
const state: LspState =
|
|
233
484
|
editors.length === 0
|
|
234
485
|
? "unknown"
|
|
@@ -240,7 +491,7 @@ export async function detectLsp(_cwd: string): Promise<LspDetection> {
|
|
|
240
491
|
state,
|
|
241
492
|
editors,
|
|
242
493
|
probed: editorClis.map((editor) => editor.cli),
|
|
243
|
-
stagingDir: join(userDataBase(
|
|
494
|
+
stagingDir: join(userDataBase(host), BIN_NAME, "lsp"),
|
|
244
495
|
};
|
|
245
496
|
}
|
|
246
497
|
|
|
@@ -259,27 +510,174 @@ export async function detectLsp(_cwd: string): Promise<LspDetection> {
|
|
|
259
510
|
export const selectedEditors = (
|
|
260
511
|
d: LspDetection,
|
|
261
512
|
chosen?: readonly string[],
|
|
262
|
-
): readonly
|
|
263
|
-
|
|
513
|
+
): readonly RunnableEditor[] => {
|
|
514
|
+
// A BLOCKED editor is never pending. It is not a candidate the user declined
|
|
515
|
+
// — it is one this command cannot act on, and composing its sideload anyway
|
|
516
|
+
// is what produced the raw fs failure the block exists to replace.
|
|
517
|
+
const pending = d.editors.filter(
|
|
518
|
+
(e): e is RunnableEditor => !e.installed && installable(e),
|
|
519
|
+
);
|
|
264
520
|
return chosen === undefined
|
|
265
521
|
? pending
|
|
266
522
|
: pending.filter((e) => chosen.includes(e.editor.cli));
|
|
267
523
|
};
|
|
268
524
|
|
|
525
|
+
/**
|
|
526
|
+
* The editors an install could act on, ignoring the wizard's selection — the
|
|
527
|
+
* question both plan rows and doctor's row ask to decide whether there is any
|
|
528
|
+
* work here at all.
|
|
529
|
+
*
|
|
530
|
+
* @param d - The detection gathered up front.
|
|
531
|
+
* @returns The editors missing the extension that can be written to.
|
|
532
|
+
*/
|
|
533
|
+
export const installableEditors = (
|
|
534
|
+
d: LspDetection,
|
|
535
|
+
): readonly RunnableEditor[] =>
|
|
536
|
+
d.editors.filter((e): e is RunnableEditor => !e.installed && installable(e));
|
|
537
|
+
|
|
269
538
|
/** The editor names in a detection (for messages/results). */
|
|
270
539
|
export const lspEditorNames = (d: LspDetection): string[] =>
|
|
271
540
|
d.editors.map((e) => e.editor.name);
|
|
272
541
|
|
|
273
542
|
/**
|
|
274
|
-
* The named-skip reason for a machine with no VS Code-family
|
|
275
|
-
* names every CLI that was probed
|
|
276
|
-
* only honest if it says what would have
|
|
543
|
+
* The named-skip reason for a machine with no VS Code-family editor at all. It
|
|
544
|
+
* names every CLI that was probed AND every place it was looked for, because a
|
|
545
|
+
* skip the user cannot act on is only honest if it says what would have
|
|
546
|
+
* counted — and "not on PATH" was a misleading half of the answer once the
|
|
547
|
+
* bundle and user-directory probes existed.
|
|
277
548
|
*
|
|
278
549
|
* @param d - The detection gathered up front.
|
|
279
550
|
* @returns The reason line.
|
|
280
551
|
*/
|
|
281
552
|
export const lspSkipReason = (d: LspDetection): string =>
|
|
282
|
-
`no VS Code-family editor
|
|
553
|
+
`no VS Code-family editor found — looked for ${d.probed.join(", ")} on PATH, under /Applications or ~/Applications, or by its user directory`;
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* How one editor was found, as a phrase both surfaces print — the provenance a
|
|
557
|
+
* user needs to tell "pragma missed my editor" from "pragma found it and could
|
|
558
|
+
* not act". Authored once, because `setup`'s child row and `doctor`'s item are
|
|
559
|
+
* the same finding.
|
|
560
|
+
*
|
|
561
|
+
* The evidence path travels with it wherever there is one: the CLI file the
|
|
562
|
+
* probe resolved. An editor found only by its user directory has no CLI, and
|
|
563
|
+
* the phrase alone is the whole finding there.
|
|
564
|
+
*
|
|
565
|
+
* @param e - One detected editor.
|
|
566
|
+
* @param roots - The two roots every path renders relative to.
|
|
567
|
+
* @returns The phrase, e.g. `via PATH (/usr/bin/codium)`.
|
|
568
|
+
*/
|
|
569
|
+
export const describeEditorSource = (
|
|
570
|
+
e: DetectedEditor,
|
|
571
|
+
roots: Roots,
|
|
572
|
+
): string => {
|
|
573
|
+
if (e.foundBy === "user-dir") return "via user directory";
|
|
574
|
+
const where = e.foundBy === "path" ? "PATH" : "app bundle";
|
|
575
|
+
return `via ${where} (${shortenPath(e.cliPath, roots)})`;
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* The home-manager declaration for a Nix-managed extensions folder.
|
|
580
|
+
*
|
|
581
|
+
* It does NOT name a marketplace id, because there is no listing — the module
|
|
582
|
+
* docblock above says so, and sideloading the bundled VSIX is the only install
|
|
583
|
+
* path that exists. So the line gives both halves a home-manager user needs:
|
|
584
|
+
* where to get the file, and the attribute that installs it.
|
|
585
|
+
*
|
|
586
|
+
* The expression is VALID NIX, which the shorter `{ src = <path>; }` sketch it
|
|
587
|
+
* replaces was not twice over: `buildVscodeExtension` has no defaults for
|
|
588
|
+
* `name`, `vscodeExtPublisher`, `vscodeExtName` or `vscodeExtUniqueId`
|
|
589
|
+
* (nixpkgs `pkgs/build-support/vscode/extensions`), and an unquoted Nix path
|
|
590
|
+
* literal cannot carry the `@` in `@canonical/…` — it is `builtins.path`
|
|
591
|
+
* around a quoted string here for that reason. `version` is the ONE attribute
|
|
592
|
+
* a user must fill in, so the line says so rather than guessing a number that
|
|
593
|
+
* would be wrong the next release.
|
|
594
|
+
*/
|
|
595
|
+
const nixDeclarationRemedy = (d: LspDetection): string =>
|
|
596
|
+
`fetch the VSIX with \`bun add ${LSP_PACKAGE}@latest\` in ${d.stagingDir}, then declare it yourself, filling in the version from the fetched package.json: programs.vscode.profiles.default.extensions = [ (pkgs.vscode-utils.buildVscodeExtension { name = "${EXTENSION_NAME}"; version = "<version>"; vscodeExtPublisher = "${EXTENSION_PUBLISHER}"; vscodeExtName = "${EXTENSION_NAME}"; vscodeExtUniqueId = "${EXTENSION_ID}"; src = builtins.path { path = "${lspVsixPath(d)}"; }; }) ];`;
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Why nothing can be installed into a blocked editor, and the ONE action that
|
|
600
|
+
* settles it — both answers from one dispatch.
|
|
601
|
+
*
|
|
602
|
+
* Two `switch`es over the same three causes (one for the reason, one for the
|
|
603
|
+
* remedy) is how a fourth cause gets a reason and no remedy; a table whose row
|
|
604
|
+
* is a `{reason, remedy}` pair cannot drift that way. The remedy is a thunk
|
|
605
|
+
* because only it needs the detection (for the staging dir and the VSIX path),
|
|
606
|
+
* and the reason is read per child row where no detection is in hand.
|
|
607
|
+
*
|
|
608
|
+
* The reason does NOT name the editor. Every place it lands already does: the
|
|
609
|
+
* setup child row is labelled `<cli> — <name> · <found via>`, doctor's item is
|
|
610
|
+
* labelled with the name, and the row headline prefixes it
|
|
611
|
+
* ({@link firstLspBlock}). Including it here made doctor print "VS Code: via
|
|
612
|
+
* user directory · VS Code is installed but has no …" — the name twice in one
|
|
613
|
+
* line and three times in two.
|
|
614
|
+
*
|
|
615
|
+
* @param e - An editor nothing can be installed into.
|
|
616
|
+
* @returns Its reason, and a remedy awaiting the detection.
|
|
617
|
+
* @note Pure — composes strings from the detection record.
|
|
618
|
+
*/
|
|
619
|
+
const describeBlock = (
|
|
620
|
+
e: BlockedEditor,
|
|
621
|
+
): { reason: string; remedy: (d: LspDetection) => string } => {
|
|
622
|
+
// Keyed by PROVENANCE, not by a block: an editor found only by its user
|
|
623
|
+
// directory has no CLI whatever its folder permits, so "no launcher" is a
|
|
624
|
+
// fact about how it was found. Its folder is never probed (see `findEditor`).
|
|
625
|
+
if (e.foundBy === "user-dir") {
|
|
626
|
+
return {
|
|
627
|
+
reason: "no command-line launcher — found only its user directory",
|
|
628
|
+
remedy: () =>
|
|
629
|
+
`run "Shell Command: Install '${e.editor.cli}' command in PATH" from ${e.editor.name}'s command palette, then run \`${BIN_NAME} setup lsp\` again`,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
switch (e.block.kind) {
|
|
633
|
+
case "nix-store":
|
|
634
|
+
return {
|
|
635
|
+
reason: `extensions folder managed by Nix (it resolves to ${e.block.resolved})`,
|
|
636
|
+
remedy: nixDeclarationRemedy,
|
|
637
|
+
};
|
|
638
|
+
case "read-only":
|
|
639
|
+
return {
|
|
640
|
+
// The folder the row names is the one a `chmod` has to reach, so the
|
|
641
|
+
// remedy can say "the extensions folder" instead of repeating it.
|
|
642
|
+
reason: `extensions folder is not writable (${e.block.path})`,
|
|
643
|
+
remedy: () =>
|
|
644
|
+
`make the extensions folder writable, then run \`${BIN_NAME} setup lsp\` again`,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Why nothing can be installed into one editor — one line, as the renderer
|
|
651
|
+
* prints one — or `undefined` when the install can act on it.
|
|
652
|
+
*
|
|
653
|
+
* @param e - One detected editor.
|
|
654
|
+
* @returns The reason, or `undefined` when the editor is actionable.
|
|
655
|
+
*/
|
|
656
|
+
export const lspBlockReason = (e: DetectedEditor): string | undefined =>
|
|
657
|
+
installable(e) ? undefined : describeBlock(e).reason;
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* The FIRST editor nothing can be installed into, with its row headline and
|
|
661
|
+
* its remedy already derived — or `undefined` when every detected editor is
|
|
662
|
+
* actionable.
|
|
663
|
+
*
|
|
664
|
+
* It hands back the derived strings rather than the editor, because the
|
|
665
|
+
* blockedness is what makes the reason and the remedy exist: a caller that
|
|
666
|
+
* took the editor and asked for them separately had to assert that both were
|
|
667
|
+
* there, twice, on both surfaces. The headline names the editor, since nothing
|
|
668
|
+
* else on a ROW has.
|
|
669
|
+
*
|
|
670
|
+
* @param d - The detection gathered up front.
|
|
671
|
+
* @returns The first block's headline and remedy, or `undefined`.
|
|
672
|
+
*/
|
|
673
|
+
export const firstLspBlock = (
|
|
674
|
+
d: LspDetection,
|
|
675
|
+
): { readonly headline: string; readonly remedy: string } | undefined => {
|
|
676
|
+
const first = blockedEditors(d).at(0);
|
|
677
|
+
if (first === undefined) return undefined;
|
|
678
|
+
const { reason, remedy } = describeBlock(first);
|
|
679
|
+
return { headline: `${first.editor.name}: ${reason}`, remedy: remedy(d) };
|
|
680
|
+
};
|
|
283
681
|
|
|
284
682
|
/**
|
|
285
683
|
* The remedy beneath that skip. It states plainly that nothing is possible here
|
|
@@ -305,7 +703,7 @@ export function composeLsp(
|
|
|
305
703
|
d: LspDetection,
|
|
306
704
|
chosen?: readonly string[],
|
|
307
705
|
): Task<void> {
|
|
308
|
-
// Nothing to install: no editor
|
|
706
|
+
// Nothing to install: no editor found at all (a named skip the plan row
|
|
309
707
|
// carries) or every detected editor already has it. Both compose NOTHING —
|
|
310
708
|
// what the run SAYS about this target is the plan row's business.
|
|
311
709
|
if (d.state !== "absent") return sequence_([]);
|
|
@@ -338,15 +736,20 @@ export function composeLsp(
|
|
|
338
736
|
]),
|
|
339
737
|
);
|
|
340
738
|
|
|
341
|
-
const sideloads = pending.map(({ editor }) => {
|
|
342
|
-
|
|
739
|
+
const sideloads = pending.map(({ editor, cliPath }) => {
|
|
740
|
+
// The CLI FILE detection resolved, which is the only thing a pending
|
|
741
|
+
// editor HAS: an editor found in its macOS app bundle has nothing on PATH
|
|
742
|
+
// to spawn, and even a PATH hit is better spawned as the file this run
|
|
743
|
+
// verified.
|
|
744
|
+
const bin = cliPath;
|
|
745
|
+
const command = `${bin} --install-extension ${vsixPath}`;
|
|
343
746
|
return guardMissingBinary(
|
|
344
|
-
|
|
747
|
+
bin,
|
|
345
748
|
{
|
|
346
|
-
message:
|
|
749
|
+
message: `${editor.name}'s CLI disappeared mid-run — restore it, then run \`${BIN_NAME} setup lsp\` again.`,
|
|
347
750
|
},
|
|
348
751
|
flatMap(
|
|
349
|
-
exec(
|
|
752
|
+
exec(bin, ["--install-extension", vsixPath], d.stagingDir),
|
|
350
753
|
(result) =>
|
|
351
754
|
checkExecOk(command, result as ExecResult, {
|
|
352
755
|
message:
|
|
@@ -372,12 +775,17 @@ export function composeLsp(
|
|
|
372
775
|
* @param d - The detection gathered up front.
|
|
373
776
|
* @returns The editors carrying a copy.
|
|
374
777
|
*/
|
|
375
|
-
export const ownedLspEditors = (d: LspDetection): readonly
|
|
376
|
-
|
|
778
|
+
export const ownedLspEditors = (d: LspDetection): readonly RunnableEditor[] =>
|
|
779
|
+
// A blocked editor is excluded even when it carries a copy: the uninstall
|
|
780
|
+
// would need a CLI this machine has not got, or a write into a directory
|
|
781
|
+
// pragma must not touch. The row says so rather than composing a reversal
|
|
782
|
+
// that cannot run.
|
|
783
|
+
d.editors.filter((e): e is RunnableEditor => e.present && installable(e));
|
|
377
784
|
|
|
378
785
|
/**
|
|
379
786
|
* Compose the removal: one `<editor cli> --uninstall-extension <id>` per owned
|
|
380
|
-
* editor, carried as the `undo` of a forward no-op
|
|
787
|
+
* editor, carried as the `undo` of a forward no-op — and each uninstall
|
|
788
|
+
* asserts its own GOAL, not just its exit code.
|
|
381
789
|
*
|
|
382
790
|
* The docblock this replaces claimed "an `exec` carries no reversal". That was
|
|
383
791
|
* factually wrong about the effect model — `Exec` has an `undo` slot and `exec`
|
|
@@ -391,37 +799,88 @@ export const ownedLspEditors = (d: LspDetection): readonly DetectedEditor[] =>
|
|
|
391
799
|
* `present` proves already exists — reads nothing and is a genuine no-op. Phase
|
|
392
800
|
* two then runs the uninstall against the real editor.
|
|
393
801
|
*
|
|
802
|
+
* THE POSTCONDITION is the removal's real success criterion. The exec's exit
|
|
803
|
+
* code and the extensions DIRECTORY are two different judges — detection
|
|
804
|
+
* deliberately reads the directory (probing via the CLI can launch an
|
|
805
|
+
* editor), and VS Code-family CLIs are known to exit 0 while deferring or
|
|
806
|
+
* skipping the deletion — so after `checkExecOk` the undo probes the exact
|
|
807
|
+
* entries detection said this command owns, and FAILS (with the manual
|
|
808
|
+
* deletion as the remedy — never the command that just claimed success) when
|
|
809
|
+
* any survived. An in-task failure here is safe precisely because
|
|
810
|
+
* `runCollectedUndos` executes each undo isolated: it used to abort every
|
|
811
|
+
* reversal still pending, which is why no removal could afford to assert its
|
|
812
|
+
* own goal.
|
|
813
|
+
*
|
|
394
814
|
* @param d - The detection gathered up front.
|
|
395
|
-
* @
|
|
815
|
+
* @param undoKey - Correlation key stamped on each reversal, echoed on its
|
|
816
|
+
* `UndoOutcome` so the caller can read the outcomes back per row.
|
|
817
|
+
* @returns A Task whose undo uninstalls the extension from each owned editor
|
|
818
|
+
* and verifies the copy is gone.
|
|
396
819
|
*/
|
|
397
|
-
export function composeLspRemoval(
|
|
820
|
+
export function composeLspRemoval(
|
|
821
|
+
d: LspDetection,
|
|
822
|
+
undoKey?: string,
|
|
823
|
+
): Task<void> {
|
|
398
824
|
const owned = ownedLspEditors(d);
|
|
399
825
|
// Nothing on this machine carries a copy — there is no honest work to model,
|
|
400
826
|
// and the plan row says so. Same shape as the other rows' empty removals.
|
|
401
827
|
if (owned.length === 0) return sequence_([]);
|
|
402
828
|
return sequence_(
|
|
403
|
-
owned.map(({ editor, extensionsDir }) => {
|
|
404
|
-
|
|
829
|
+
owned.map(({ editor, cliPath, extensionsDir, presentEntries }) => {
|
|
830
|
+
// The resolved CLI file, for the reason `composeLsp` spells out.
|
|
831
|
+
const bin = cliPath;
|
|
832
|
+
const command = `${bin} --uninstall-extension ${EXTENSION_ID}`;
|
|
833
|
+
const ownedPaths = presentEntries.map((entry) =>
|
|
834
|
+
join(extensionsDir, entry),
|
|
835
|
+
);
|
|
836
|
+
const uninstall = flatMap(
|
|
837
|
+
exec(bin, ["--uninstall-extension", EXTENSION_ID], extensionsDir),
|
|
838
|
+
(result) =>
|
|
839
|
+
checkExecOk(command, result as ExecResult, {
|
|
840
|
+
message:
|
|
841
|
+
`${editor.name} refused the uninstall (its output is above). ` +
|
|
842
|
+
`Remove it by hand with \`${command}\`.`,
|
|
843
|
+
}),
|
|
844
|
+
);
|
|
845
|
+
// The probe must apply DETECTION'S presence criterion, or the check can
|
|
846
|
+
// quietly stop checking: `extensionState` calls an entry present when
|
|
847
|
+
// `readdirSync` lists it, and a default `exists` (`fs.access`) follows
|
|
848
|
+
// symlinks — so a dangling matching symlink would be "installed" to
|
|
849
|
+
// detection and "absent" here, and an editor that exits 0 without
|
|
850
|
+
// removing it would report `undone` while the next detection finds it
|
|
851
|
+
// again. `followSymlinks: false` is `lstat` semantics: the entry
|
|
852
|
+
// itself, exactly what the directory listing answers.
|
|
853
|
+
const verified = flatMap(uninstall, () =>
|
|
854
|
+
flatMap(
|
|
855
|
+
sequence(ownedPaths.map((p) => exists(p, { followSymlinks: false }))),
|
|
856
|
+
(stillThere) => {
|
|
857
|
+
const survivors = ownedPaths.filter((_, i) => stillThere[i]);
|
|
858
|
+
if (survivors.length === 0) return pure(undefined);
|
|
859
|
+
// The remedy presents the surviving paths as DATA, never as a
|
|
860
|
+
// synthesized shell line: the names come from `readdirSync`, so
|
|
861
|
+
// no quoting is trustworthy, and one command cannot be right for
|
|
862
|
+
// every platform the editor registry supports.
|
|
863
|
+
return failPragma(
|
|
864
|
+
new PragmaError({
|
|
865
|
+
code: "UNSUPPORTED",
|
|
866
|
+
message: `\`${command}\` exited 0, but the extension is still present after the undo ran.`,
|
|
867
|
+
recovery: {
|
|
868
|
+
message: `Delete ${survivors.length === 1 ? "this entry" : "these entries"} from ${editor.name}'s extensions folder by hand, then restart it: ${survivors.join(" · ")}`,
|
|
869
|
+
},
|
|
870
|
+
}),
|
|
871
|
+
);
|
|
872
|
+
},
|
|
873
|
+
),
|
|
874
|
+
);
|
|
405
875
|
return mkdir(extensionsDir, true, {
|
|
406
876
|
undo: guardMissingBinary(
|
|
407
|
-
|
|
877
|
+
bin,
|
|
408
878
|
{
|
|
409
|
-
message:
|
|
879
|
+
message: `${editor.name}'s CLI disappeared mid-run — restore it, then run \`${BIN_NAME} setup lsp --undo\` again.`,
|
|
410
880
|
},
|
|
411
|
-
|
|
412
|
-
exec(
|
|
413
|
-
editor.cli,
|
|
414
|
-
["--uninstall-extension", EXTENSION_ID],
|
|
415
|
-
extensionsDir,
|
|
416
|
-
),
|
|
417
|
-
(result) =>
|
|
418
|
-
checkExecOk(command, result as ExecResult, {
|
|
419
|
-
message:
|
|
420
|
-
`${editor.name} refused the uninstall (its output is above). ` +
|
|
421
|
-
`Remove it by hand with \`${command}\`.`,
|
|
422
|
-
}),
|
|
423
|
-
),
|
|
881
|
+
verified,
|
|
424
882
|
),
|
|
883
|
+
undoKey,
|
|
425
884
|
});
|
|
426
885
|
}),
|
|
427
886
|
);
|
|
@@ -441,10 +900,10 @@ export function composeLspRemoval(d: LspDetection): Task<void> {
|
|
|
441
900
|
* @returns The uninstall command, or undefined.
|
|
442
901
|
*/
|
|
443
902
|
export function lspUninstallRemedy(d: LspDetection): string | undefined {
|
|
444
|
-
const
|
|
445
|
-
return
|
|
903
|
+
const owner = ownedLspEditors(d).at(0);
|
|
904
|
+
return owner === undefined
|
|
446
905
|
? undefined
|
|
447
|
-
: `${
|
|
906
|
+
: `${owner.cliPath} --uninstall-extension ${EXTENSION_ID}`;
|
|
448
907
|
}
|
|
449
908
|
|
|
450
909
|
/** The VSIX path a failed sideload leaves behind for a manual retry. */
|