@p4code/cli 0.0.42 → 0.0.44
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/dist/bin.mjs +290 -183
- package/dist/client/assets/DiffPanel-DD3E29O7.js +98 -0
- package/dist/client/assets/FilePreviewPanel-fBOIzVAm.js +1778 -0
- package/dist/client/assets/PreviewPanel-BXyhScZZ.js +2 -0
- package/dist/client/assets/{angular-html-D2LtifxG.js → angular-html-BwUZQlj7.js} +2 -2
- package/dist/client/assets/{angular-ts-oVDpKJb2.js → angular-ts-8EX8ZNeQ.js} +2 -2
- package/dist/client/assets/{apl-2vnm7rdp.js → apl-DYm7AarA.js} +2 -2
- package/dist/client/assets/arrow-right-BNeeftk2.js +2 -0
- package/dist/client/assets/{astro-CDbEfI_f.js → astro-DFaFMvm9.js} +2 -2
- package/dist/client/assets/{blade-B1MwvSO0.js → blade-D9aigeeZ.js} +2 -2
- package/dist/client/assets/{c-Bi5eHJiI.js → c-BvAsa11Y.js} +2 -2
- package/dist/client/assets/{cobol-BxESWi7D.js → cobol-BZwY3--F.js} +2 -2
- package/dist/client/assets/{coffee-s151wUsp.js → coffee-vNqcHwuy.js} +2 -2
- package/dist/client/assets/{cpp-B51xjAkw.js → cpp-D3Xet0_6.js} +2 -2
- package/dist/client/assets/{crystal-BW0dq2Rw.js → crystal-D-UW0VhN.js} +2 -2
- package/dist/client/assets/{css-Czn5ycv4.js → css-B1CDNsy0.js} +2 -2
- package/dist/client/assets/{edge-DPtVbjt1.js → edge-B_geUcoi.js} +2 -2
- package/dist/client/assets/{elixir-381Dy51E.js → elixir-BzK0Ks1B.js} +2 -2
- package/dist/client/assets/{elm-Bpu_ckb9.js → elm-BvlMiaGz.js} +2 -2
- package/dist/client/assets/{erb-u1vpMXOw.js → erb-OFCHFHP8.js} +2 -2
- package/dist/client/assets/fileCommentAnnotations-DI_Ec6DZ.js +2 -0
- package/dist/client/assets/{git-rebase-ShzdQ6mA.js → git-rebase-Xc_wijNW.js} +2 -2
- package/dist/client/assets/{glimmer-js-DI4_LeGA.js → glimmer-js-D-GpFszK.js} +2 -2
- package/dist/client/assets/{glimmer-ts-BrA1R4ia.js → glimmer-ts-_MmK7qB5.js} +2 -2
- package/dist/client/assets/{glsl--BAUZuR7.js → glsl-Cm_ctoze.js} +2 -2
- package/dist/client/assets/{graphql-XmtNe4tX.js → graphql-De_-LrEt.js} +2 -2
- package/dist/client/assets/{hack-ZPcuqt6I.js → hack-qUzK29KK.js} +2 -2
- package/dist/client/assets/{haml-BLmOYSAV.js → haml-mqyUpOBj.js} +2 -2
- package/dist/client/assets/{handlebars-BxR19vQc.js → handlebars-DS3jjsXS.js} +2 -2
- package/dist/client/assets/{html-derivative-CLJquIOq.js → html-derivative-BPRW_sHn.js} +2 -2
- package/dist/client/assets/{html-Do3TWS07.js → html-ippoHerj.js} +2 -2
- package/dist/client/assets/{http-C6M7KGrt.js → http-xrFfvFRg.js} +2 -2
- package/dist/client/assets/{hurl-AFYDoSjV.js → hurl-DSL-wjHh.js} +2 -2
- package/dist/client/assets/index-BAsvpSiM.js +636 -0
- package/dist/client/assets/index-CGyzLTcp.css +1 -0
- package/dist/client/assets/{java-DoRNyNZ_.js → java-B8xLHoVB.js} +2 -2
- package/dist/client/assets/{javascript-Dd-zEQJs.js → javascript-DkxetjtW.js} +2 -2
- package/dist/client/assets/{jinja-TJRtEusk.js → jinja-DrlmZXOI.js} +2 -2
- package/dist/client/assets/{jison-bG9mcEJe.js → jison-CBYMCWiZ.js} +2 -2
- package/dist/client/assets/{json-DY6EaQUk.js → json-aobk_LSd.js} +2 -2
- package/dist/client/assets/{jsx-BuL0_m-Z.js → jsx-BTcIKPoF.js} +2 -2
- package/dist/client/assets/{julia-4bcuyKPV.js → julia-BwUvFb1K.js} +2 -2
- package/dist/client/assets/{just-F3UNctxi.js → just-2GL8m28O.js} +2 -2
- package/dist/client/assets/{latex-DCJY-Aag.js → latex-BwWZNh8B.js} +2 -2
- package/dist/client/assets/{liquid-M906bGYJ.js → liquid-j8UWR5fn.js} +2 -2
- package/dist/client/assets/{lua-CeupCtSb.js → lua-BiEJazUk.js} +2 -2
- package/dist/client/assets/{marko-D_3SsXZn.js → marko-BrkuG9X9.js} +2 -2
- package/dist/client/assets/{mdc-DRUaGPVE.js → mdc-CbE9Gm1n.js} +2 -2
- package/dist/client/assets/{nginx-B_DJSFb0.js → nginx-C-gcDf_H.js} +2 -2
- package/dist/client/assets/{nim-BxoRFkNd.js → nim-9X20IMJZ.js} +2 -2
- package/dist/client/assets/{perl-CjLiNsGL.js → perl-Dqj-SUzY.js} +2 -2
- package/dist/client/assets/{php-DcklQON0.js → php-CQymwCam.js} +2 -2
- package/dist/client/assets/{pug-DHYHUkbl.js → pug-DIOYJ7Tj.js} +2 -2
- package/dist/client/assets/{qml-Cggn-sFR.js → qml-BpdlPbMh.js} +2 -2
- package/dist/client/assets/{r-BhkOaKoM.js → r-BdXpML7C.js} +2 -2
- package/dist/client/assets/{razor-DksRt091.js → razor-CmGIbQUz.js} +2 -2
- package/dist/client/assets/{regexp-CVeVJKpO.js → regexp-udR9FXf8.js} +2 -2
- package/dist/client/assets/{rst-B8gnyB7p.js → rst-C81pNvVo.js} +2 -2
- package/dist/client/assets/{ruby-B7aYI7EY.js → ruby-D5xBAIVx.js} +2 -2
- package/dist/client/assets/{sas-DmDsVHsr.js → sas-pegDgwhF.js} +2 -2
- package/dist/client/assets/{scss-Cf0hFbmP.js → scss-Dx_Aq9w8.js} +2 -2
- package/dist/client/assets/{shellscript-CJ2l7Ljo.js → shellscript-Bl-HTaUh.js} +2 -2
- package/dist/client/assets/{shellsession-CE-6mg0T.js → shellsession-CezqakSW.js} +2 -2
- package/dist/client/assets/{soy-eTwUOhLL.js → soy-DclELXc7.js} +2 -2
- package/dist/client/assets/{sql-DaTGJ1E1.js → sql-DAsY-zzx.js} +2 -2
- package/dist/client/assets/{stata-DCqqmU-V.js → stata-DjQ16uuy.js} +2 -2
- package/dist/client/assets/{surrealql-DJuyQLb6.js → surrealql-BLzgjzgE.js} +2 -2
- package/dist/client/assets/{svelte-DZfElFr5.js → svelte-DFGwC3eP.js} +2 -2
- package/dist/client/assets/{templ-BeuT7j1S.js → templ-BiFbdCJD.js} +2 -2
- package/dist/client/assets/{tex-CUjxQ3iG.js → tex-DQCF2PEO.js} +2 -2
- package/dist/client/assets/textarea-lBMORu4g.js +46 -0
- package/dist/client/assets/{ts-tags-C00W--Ve.js → ts-tags-CkQC6ekt.js} +2 -2
- package/dist/client/assets/{tsx-h-kXP2gA.js → tsx-m7HWHXfL.js} +2 -2
- package/dist/client/assets/{twig-DhMoAQY-.js → twig-B5P5jotH.js} +2 -2
- package/dist/client/assets/{typescript-DwJdYtiC.js → typescript-DmGwuo7e.js} +2 -2
- package/dist/client/assets/{vue-CvLnzHbi.js → vue-Rn_gU7Px.js} +2 -2
- package/dist/client/assets/{vue-html-fj2AgeR4.js → vue-html-DbKQF2OX.js} +2 -2
- package/dist/client/assets/{vue-vine-D1-Lx9ZR.js → vue-vine-C13tIrLE.js} +2 -2
- package/dist/client/assets/{xml-CDkXSKu_.js → xml-D7fQm-eG.js} +2 -2
- package/dist/client/assets/{xsl-BkotjMT0.js → xsl-Bv743my_.js} +2 -2
- package/dist/client/assets/{yaml-BssI7mmn.js → yaml--h7pPfkH.js} +2 -2
- package/dist/client/index.html +3 -3
- package/package.json +1 -1
- package/dist/client/assets/DiffPanel-D8nVXZ0Y.js +0 -98
- package/dist/client/assets/FilePreviewPanel-CFKCFyJ3.js +0 -1778
- package/dist/client/assets/PreviewPanel-VQey9906.js +0 -2
- package/dist/client/assets/arrow-right-DmJqM7J1.js +0 -2
- package/dist/client/assets/fileCommentAnnotations-BwnrRpfq.js +0 -2
- package/dist/client/assets/index-XxxL_T8f.js +0 -635
- package/dist/client/assets/index-mqpX0B5G.css +0 -1
- package/dist/client/assets/textarea-BM2z9WNB.js +0 -46
package/dist/bin.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import * as Console from "effect/Console";
|
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
8
|
import * as Layer from "effect/Layer";
|
|
9
9
|
import { Argument, Command, Flag, GlobalFlag, Prompt } from "effect/unstable/cli";
|
|
10
|
+
import * as CliError from "effect/unstable/cli/CliError";
|
|
10
11
|
import { FetchHttpClient, Headers, HttpBody, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, HttpRouter, HttpServer, HttpServerRequest, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http";
|
|
11
12
|
import * as Context from "effect/Context";
|
|
12
13
|
import * as NodeOS from "node:os";
|
|
@@ -232,7 +233,7 @@ const make$71 = () => {
|
|
|
232
233
|
const layer$67 = Layer.sync(NetService, make$71);
|
|
233
234
|
//#endregion
|
|
234
235
|
//#region package.json
|
|
235
|
-
var version = "0.0.
|
|
236
|
+
var version = "0.0.44";
|
|
236
237
|
//#endregion
|
|
237
238
|
//#region src/config.ts
|
|
238
239
|
/**
|
|
@@ -13924,9 +13925,10 @@ const HubHttpServerLive = Layer.unwrap(Effect.gen(function* () {
|
|
|
13924
13925
|
/**
|
|
13925
13926
|
* Says on startup exactly what the hub is exposing and to whom.
|
|
13926
13927
|
*
|
|
13927
|
-
* A hub bound past loopback is reachable by
|
|
13928
|
-
*
|
|
13929
|
-
* deliberate configuration, but it should
|
|
13928
|
+
* A hub bound past loopback is reachable by whatever can route to it — on the
|
|
13929
|
+
* hosted deployment, the public internet — and the bearer token is then the
|
|
13930
|
+
* only remaining control. That is a deliberate configuration, but it should
|
|
13931
|
+
* never be a silent one.
|
|
13930
13932
|
*/
|
|
13931
13933
|
const announce = Layer.effectDiscard(Effect.gen(function* () {
|
|
13932
13934
|
const config = yield* HubConfig;
|
|
@@ -13938,7 +13940,7 @@ const announce = Layer.effectDiscard(Effect.gen(function* () {
|
|
|
13938
13940
|
dbPath: config.dbPath,
|
|
13939
13941
|
authorizedServers: config.tokenHashes.size
|
|
13940
13942
|
});
|
|
13941
|
-
if (isRemoteReachableHost(config.host)) yield* Effect.logWarning("Hub is
|
|
13943
|
+
if (isRemoteReachableHost(config.host)) yield* Effect.logWarning("Hub is reachable beyond loopback. The bearer token is the only control — repeated auth failures are not rate limited.", { host: config.host });
|
|
13942
13944
|
}));
|
|
13943
13945
|
const makeHubLayer = HttpRouter.serve(layer$58).pipe(Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
|
|
13944
13946
|
const runHub = Layer.launch(makeHubLayer);
|
|
@@ -14905,15 +14907,188 @@ const removeMcpRegistration = Effect.fn("mcpRegistrationFiles.remove")(function*
|
|
|
14905
14907
|
yield* fileSystem.remove(path.join(input.registrationsRoot, input.name), { force: true });
|
|
14906
14908
|
});
|
|
14907
14909
|
//#endregion
|
|
14910
|
+
//#region src/sync/skillDirectory.ts
|
|
14911
|
+
/**
|
|
14912
|
+
* Reading and writing skill directories on disk.
|
|
14913
|
+
*
|
|
14914
|
+
* Kept apart from the sync itself so the filesystem shape has its own tests,
|
|
14915
|
+
* and so the rules about what is refusable — binaries, oversized files, deep
|
|
14916
|
+
* trees — live in one place rather than being rediscovered at each call site.
|
|
14917
|
+
*
|
|
14918
|
+
* @module sync/skillDirectory
|
|
14919
|
+
*/
|
|
14920
|
+
/** Bounded so a stray symlink or a vendored `node_modules` cannot walk forever. */
|
|
14921
|
+
const MAX_SKILL_DEPTH = 4;
|
|
14922
|
+
/**
|
|
14923
|
+
* Entries that are never part of a skill, skipped rather than refused.
|
|
14924
|
+
*
|
|
14925
|
+
* The distinction matters. Refusing an asset because it cannot be represented
|
|
14926
|
+
* faithfully is right — a half-materialized skill loads and misbehaves. But a
|
|
14927
|
+
* `.DS_Store` is not part of the skill at all, and macOS drops one into any
|
|
14928
|
+
* directory that has been opened in Finder, so refusing on its account rejects
|
|
14929
|
+
* a perfectly good skill for a reason its author cannot see.
|
|
14930
|
+
*
|
|
14931
|
+
* These could not sync in any case: `AgentAssetFilePath` rejects a leading dot,
|
|
14932
|
+
* so a dotfile has no representable path even if it were text.
|
|
14933
|
+
*/
|
|
14934
|
+
const IGNORED_ENTRIES = /* @__PURE__ */ new Set([
|
|
14935
|
+
".DS_Store",
|
|
14936
|
+
"Thumbs.db",
|
|
14937
|
+
".git",
|
|
14938
|
+
".svn",
|
|
14939
|
+
".hg",
|
|
14940
|
+
"node_modules",
|
|
14941
|
+
"__pycache__",
|
|
14942
|
+
".venv",
|
|
14943
|
+
".pytest_cache",
|
|
14944
|
+
".ruff_cache",
|
|
14945
|
+
".mypy_cache"
|
|
14946
|
+
]);
|
|
14947
|
+
/**
|
|
14948
|
+
* Anything else with a leading dot is skipped too — an editor's `.idea`, a
|
|
14949
|
+
* `.gitignore` that belongs to the checkout rather than the skill. The rule is
|
|
14950
|
+
* the same one the path schema already enforces, applied earlier so it reads as
|
|
14951
|
+
* "not part of the skill" instead of "this skill is broken".
|
|
14952
|
+
*/
|
|
14953
|
+
const isIgnoredEntry = (entry) => IGNORED_ENTRIES.has(entry) || entry.startsWith(".");
|
|
14954
|
+
/**
|
|
14955
|
+
* A file is binary if decoding it yields a NUL.
|
|
14956
|
+
*
|
|
14957
|
+
* Cheap, and right for what is being asked: the question is not "is this valid
|
|
14958
|
+
* UTF-8" but "can this round-trip through a JSON text column", and a NUL is
|
|
14959
|
+
* what actually breaks that on the way back out of SQLite.
|
|
14960
|
+
*/
|
|
14961
|
+
const looksBinary$1 = (content) => content.includes("\0");
|
|
14962
|
+
/**
|
|
14963
|
+
* Read one directory as an asset's files, or say why it cannot be one.
|
|
14964
|
+
*
|
|
14965
|
+
* Exported because memory's `global-memory/` is the same shape by a different
|
|
14966
|
+
* name: a directory whose whole contents are the asset. Sharing this keeps the
|
|
14967
|
+
* refusal rules — depth, file count, binaries, size — identical for both,
|
|
14968
|
+
* rather than letting a second copy drift into accepting what the first
|
|
14969
|
+
* rejects.
|
|
14970
|
+
*/
|
|
14971
|
+
const readSkillFiles = Effect.fn("skillDirectory.readSkillFiles")(function* (root) {
|
|
14972
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
14973
|
+
const path = yield* Path.Path;
|
|
14974
|
+
const files = [];
|
|
14975
|
+
const walk = Effect.fn("skillDirectory.walk")(function* (directory, prefix, depth) {
|
|
14976
|
+
if (depth > MAX_SKILL_DEPTH) return "too-deep";
|
|
14977
|
+
const entries = yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []));
|
|
14978
|
+
for (const entry of [...entries].sort()) {
|
|
14979
|
+
if (isIgnoredEntry(entry)) continue;
|
|
14980
|
+
const absolute = path.join(directory, entry);
|
|
14981
|
+
const relative = prefix === "" ? entry : `${prefix}/${entry}`;
|
|
14982
|
+
const info = yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0));
|
|
14983
|
+
if (info === void 0) return "unreadable";
|
|
14984
|
+
if (info.type === "Directory") {
|
|
14985
|
+
const nested = yield* walk(absolute, relative, depth + 1);
|
|
14986
|
+
if (nested !== null) return nested;
|
|
14987
|
+
continue;
|
|
14988
|
+
}
|
|
14989
|
+
if (info.type !== "File") continue;
|
|
14990
|
+
if (files.length >= 512) return "too-many-files";
|
|
14991
|
+
const content = yield* fileSystem.readFileString(absolute).pipe(Effect.orElseSucceed(() => void 0));
|
|
14992
|
+
if (content === void 0) return "unreadable";
|
|
14993
|
+
if (looksBinary$1(content)) return "binary-file";
|
|
14994
|
+
if (content.length > 524288) return "file-too-large";
|
|
14995
|
+
files.push({
|
|
14996
|
+
path: relative,
|
|
14997
|
+
content
|
|
14998
|
+
});
|
|
14999
|
+
}
|
|
15000
|
+
return null;
|
|
15001
|
+
});
|
|
15002
|
+
const refusal = yield* walk(root, "", 1);
|
|
15003
|
+
if (refusal !== null) return { refusal };
|
|
15004
|
+
if (agentAssetTotalBytes(files) > 4194304) return { refusal: "asset-too-large" };
|
|
15005
|
+
return { files };
|
|
15006
|
+
});
|
|
15007
|
+
/**
|
|
15008
|
+
* Read every skill directory under a user-scope `skills/` root.
|
|
15009
|
+
*
|
|
15010
|
+
* A directory with no `SKILL.md` is not a skill and is skipped silently —
|
|
15011
|
+
* Claude Code would ignore it too, so reporting it would be noise about
|
|
15012
|
+
* something that was never a skill in the first place.
|
|
15013
|
+
*/
|
|
15014
|
+
const readSkillDirectory = Effect.fn("skillDirectory.readSkillDirectory")(function* (skillsRoot) {
|
|
15015
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
15016
|
+
const path = yield* Path.Path;
|
|
15017
|
+
const entries = yield* fileSystem.readDirectory(skillsRoot).pipe(Effect.orElseSucceed(() => []));
|
|
15018
|
+
const skills = [];
|
|
15019
|
+
const refused = [];
|
|
15020
|
+
for (const entry of [...entries].sort()) {
|
|
15021
|
+
if (isIgnoredEntry(entry)) continue;
|
|
15022
|
+
const directory = path.join(skillsRoot, entry);
|
|
15023
|
+
if ((yield* fileSystem.stat(directory).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "Directory") continue;
|
|
15024
|
+
if (!(yield* fileSystem.exists(path.join(directory, "SKILL.md")).pipe(Effect.orElseSucceed(() => false)))) continue;
|
|
15025
|
+
const read = yield* readSkillFiles(directory);
|
|
15026
|
+
if ("refusal" in read) {
|
|
15027
|
+
refused.push({
|
|
15028
|
+
name: entry,
|
|
15029
|
+
reason: read.refusal
|
|
15030
|
+
});
|
|
15031
|
+
continue;
|
|
15032
|
+
}
|
|
15033
|
+
skills.push({
|
|
15034
|
+
name: entry,
|
|
15035
|
+
files: read.files
|
|
15036
|
+
});
|
|
15037
|
+
}
|
|
15038
|
+
return {
|
|
15039
|
+
skills,
|
|
15040
|
+
refused
|
|
15041
|
+
};
|
|
15042
|
+
});
|
|
15043
|
+
/**
|
|
15044
|
+
* Replace a skill directory with exactly the asset's files.
|
|
15045
|
+
*
|
|
15046
|
+
* Written to a sibling and then swapped, so a crash mid-write leaves the old
|
|
15047
|
+
* skill intact rather than half of the new one. A skill is loaded as a unit and
|
|
15048
|
+
* a partially written one is instructions with the middle missing, which an
|
|
15049
|
+
* agent will happily follow.
|
|
15050
|
+
*/
|
|
15051
|
+
const writeSkillDirectory = Effect.fn("skillDirectory.writeSkillDirectory")(function* (input) {
|
|
15052
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
15053
|
+
const path = yield* Path.Path;
|
|
15054
|
+
const target = path.join(input.skillsRoot, input.name);
|
|
15055
|
+
const staging = path.join(input.skillsRoot, `.${input.name}.p4code-staging`);
|
|
15056
|
+
yield* fileSystem.makeDirectory(input.skillsRoot, { recursive: true });
|
|
15057
|
+
yield* fileSystem.remove(staging, {
|
|
15058
|
+
recursive: true,
|
|
15059
|
+
force: true
|
|
15060
|
+
});
|
|
15061
|
+
yield* fileSystem.makeDirectory(staging, { recursive: true });
|
|
15062
|
+
for (const file of input.files) {
|
|
15063
|
+
const destination = path.join(staging, file.path);
|
|
15064
|
+
yield* fileSystem.makeDirectory(path.dirname(destination), { recursive: true });
|
|
15065
|
+
yield* fileSystem.writeFileString(destination, file.content);
|
|
15066
|
+
}
|
|
15067
|
+
yield* fileSystem.remove(target, {
|
|
15068
|
+
recursive: true,
|
|
15069
|
+
force: true
|
|
15070
|
+
});
|
|
15071
|
+
yield* fileSystem.rename(staging, target);
|
|
15072
|
+
});
|
|
15073
|
+
const removeSkillDirectory = Effect.fn("skillDirectory.removeSkillDirectory")(function* (input) {
|
|
15074
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
15075
|
+
const path = yield* Path.Path;
|
|
15076
|
+
yield* fileSystem.remove(path.join(input.skillsRoot, input.name), {
|
|
15077
|
+
recursive: true,
|
|
15078
|
+
force: true
|
|
15079
|
+
});
|
|
15080
|
+
});
|
|
15081
|
+
//#endregion
|
|
14908
15082
|
//#region src/sync/memoryFile.ts
|
|
14909
15083
|
/**
|
|
14910
15084
|
* Reading and writing user-scope memory files.
|
|
14911
15085
|
*
|
|
14912
15086
|
* Memory is the instruction file an agent reads before anything else, so a
|
|
14913
15087
|
* machine whose `CLAUDE.md` differs is a machine whose agent behaves
|
|
14914
|
-
* differently — which is the whole reason the hub exists.
|
|
14915
|
-
*
|
|
14916
|
-
* rather than in a subdirectory the sync could own outright
|
|
15088
|
+
* differently — which is the whole reason the hub exists. It comes in two
|
|
15089
|
+
* shapes: the entry-point files, which sit *directly* in the provider's config
|
|
15090
|
+
* directory rather than in a subdirectory the sync could own outright, and the
|
|
15091
|
+
* directory of rules those files include by reference.
|
|
14917
15092
|
*
|
|
14918
15093
|
* That last part is the entire design constraint. `<configDir>` also holds
|
|
14919
15094
|
* `settings.json`, `.claude.json` and `.credentials.json`. "Write the file this
|
|
@@ -14950,9 +15125,38 @@ const MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md"];
|
|
|
14950
15125
|
* it on a case-sensitive one would make the same hub row materialize on one
|
|
14951
15126
|
* machine and refuse on another.
|
|
14952
15127
|
*/
|
|
14953
|
-
const
|
|
15128
|
+
const isMemoryFileName = (name) => MEMORY_FILE_NAMES.includes(name);
|
|
15129
|
+
/**
|
|
15130
|
+
* The directories in the config directory this sync will carry whole.
|
|
15131
|
+
*
|
|
15132
|
+
* A memory file is rarely the whole of the memory. `CLAUDE.md` is written to be
|
|
15133
|
+
* an index — a dozen `@~/.claude/global-memory/...` lines and little else — so
|
|
15134
|
+
* syncing the file alone hands the other machine a list of pointers to files
|
|
15135
|
+
* that are not there, and an agent reading it behaves as if the rules it names
|
|
15136
|
+
* do not exist. That failure is silent: the includes resolve to nothing rather
|
|
15137
|
+
* than to an error.
|
|
15138
|
+
*
|
|
15139
|
+
* So this is an allowlist too, and for the same reason the file one exists. The
|
|
15140
|
+
* escape it prevents is different, though, and weaker: `AgentAssetFilePath`
|
|
15141
|
+
* already forbids `..`, leading slashes and backslashes, so a directory asset
|
|
15142
|
+
* cannot write outside its own directory whatever it names its files. What the
|
|
15143
|
+
* allowlist adds is that the *directory* cannot be named freely either — an
|
|
15144
|
+
* asset is not permitted to claim `.claude/projects` or any other directory
|
|
15145
|
+
* that already means something to the provider.
|
|
15146
|
+
*/
|
|
15147
|
+
const MEMORY_DIRECTORY_NAMES = ["global-memory"];
|
|
15148
|
+
/** Whether this name is one of the memory directories. Exact match, as above. */
|
|
15149
|
+
const isMemoryDirectoryName = (name) => MEMORY_DIRECTORY_NAMES.includes(name);
|
|
15150
|
+
/**
|
|
15151
|
+
* Whether this name is memory of either shape.
|
|
15152
|
+
*
|
|
15153
|
+
* The two lists must stay disjoint, and are: an asset that is a file in one and
|
|
15154
|
+
* a directory in the other would have the writer delete a file to put a
|
|
15155
|
+
* directory in its place, or the reverse, depending on which machine ran last.
|
|
15156
|
+
*/
|
|
15157
|
+
const isMemoryAssetName = (name) => isMemoryFileName(name) || isMemoryDirectoryName(name);
|
|
14954
15158
|
/** The same test the skill reader uses: a NUL is what breaks a JSON text column. */
|
|
14955
|
-
const looksBinary
|
|
15159
|
+
const looksBinary = (content) => content.includes("\0");
|
|
14956
15160
|
/**
|
|
14957
15161
|
* Read whichever memory files exist in this machine's config directory.
|
|
14958
15162
|
*
|
|
@@ -14976,7 +15180,7 @@ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memor
|
|
|
14976
15180
|
});
|
|
14977
15181
|
continue;
|
|
14978
15182
|
}
|
|
14979
|
-
if (looksBinary
|
|
15183
|
+
if (looksBinary(content)) {
|
|
14980
15184
|
refused.push({
|
|
14981
15185
|
name,
|
|
14982
15186
|
reason: "binary-file"
|
|
@@ -15006,6 +15210,23 @@ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memor
|
|
|
15006
15210
|
files
|
|
15007
15211
|
});
|
|
15008
15212
|
}
|
|
15213
|
+
for (const name of MEMORY_DIRECTORY_NAMES) {
|
|
15214
|
+
const absolute = path.join(memoryRoot, name);
|
|
15215
|
+
if ((yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "Directory") continue;
|
|
15216
|
+
const read = yield* readSkillFiles(absolute);
|
|
15217
|
+
if ("refusal" in read) {
|
|
15218
|
+
refused.push({
|
|
15219
|
+
name,
|
|
15220
|
+
reason: read.refusal
|
|
15221
|
+
});
|
|
15222
|
+
continue;
|
|
15223
|
+
}
|
|
15224
|
+
if (read.files.length === 0) continue;
|
|
15225
|
+
skills.push({
|
|
15226
|
+
name,
|
|
15227
|
+
files: read.files
|
|
15228
|
+
});
|
|
15229
|
+
}
|
|
15009
15230
|
return {
|
|
15010
15231
|
skills,
|
|
15011
15232
|
refused
|
|
@@ -15019,7 +15240,10 @@ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memor
|
|
|
15019
15240
|
* carrying a second file called `settings.json` would otherwise write it, which
|
|
15020
15241
|
* is the same escape by a longer route.
|
|
15021
15242
|
*/
|
|
15022
|
-
const isWritableMemoryAsset = (input) =>
|
|
15243
|
+
const isWritableMemoryAsset = (input) => {
|
|
15244
|
+
if (isMemoryDirectoryName(input.name)) return true;
|
|
15245
|
+
return isMemoryFileName(input.name) && input.files.length === 1 && input.files[0]?.path === input.name;
|
|
15246
|
+
};
|
|
15023
15247
|
/**
|
|
15024
15248
|
* Write a memory file, or refuse.
|
|
15025
15249
|
*
|
|
@@ -15033,6 +15257,11 @@ const writeMemoryFile = Effect.fn("memoryFile.writeMemoryFile")(function* (input
|
|
|
15033
15257
|
if (!isWritableMemoryAsset(input)) return "refused";
|
|
15034
15258
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
15035
15259
|
const path = yield* Path.Path;
|
|
15260
|
+
if (isMemoryDirectoryName(input.name)) return yield* writeSkillDirectory({
|
|
15261
|
+
skillsRoot: input.memoryRoot,
|
|
15262
|
+
name: input.name,
|
|
15263
|
+
files: input.files
|
|
15264
|
+
}).pipe(Effect.as("written"), Effect.orElseSucceed(() => "failed"));
|
|
15036
15265
|
const contents = input.files[0]?.content ?? "";
|
|
15037
15266
|
yield* fileSystem.makeDirectory(input.memoryRoot, { recursive: true }).pipe(Effect.ignore);
|
|
15038
15267
|
return yield* writeFileStringAtomically({
|
|
@@ -15042,6 +15271,13 @@ const writeMemoryFile = Effect.fn("memoryFile.writeMemoryFile")(function* (input
|
|
|
15042
15271
|
});
|
|
15043
15272
|
const removeMemoryFile = Effect.fn("memoryFile.removeMemoryFile")(function* (input) {
|
|
15044
15273
|
if (!isMemoryAssetName(input.name)) return;
|
|
15274
|
+
if (isMemoryDirectoryName(input.name)) {
|
|
15275
|
+
yield* removeSkillDirectory({
|
|
15276
|
+
skillsRoot: input.memoryRoot,
|
|
15277
|
+
name: input.name
|
|
15278
|
+
});
|
|
15279
|
+
return;
|
|
15280
|
+
}
|
|
15045
15281
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
15046
15282
|
const path = yield* Path.Path;
|
|
15047
15283
|
yield* fileSystem.remove(path.join(input.memoryRoot, input.name), { force: true });
|
|
@@ -15107,169 +15343,6 @@ const writeSkillManifest = Effect.fn("skillManifest.write")(function* (manifestP
|
|
|
15107
15343
|
});
|
|
15108
15344
|
});
|
|
15109
15345
|
//#endregion
|
|
15110
|
-
//#region src/sync/skillDirectory.ts
|
|
15111
|
-
/**
|
|
15112
|
-
* Reading and writing skill directories on disk.
|
|
15113
|
-
*
|
|
15114
|
-
* Kept apart from the sync itself so the filesystem shape has its own tests,
|
|
15115
|
-
* and so the rules about what is refusable — binaries, oversized files, deep
|
|
15116
|
-
* trees — live in one place rather than being rediscovered at each call site.
|
|
15117
|
-
*
|
|
15118
|
-
* @module sync/skillDirectory
|
|
15119
|
-
*/
|
|
15120
|
-
/** Bounded so a stray symlink or a vendored `node_modules` cannot walk forever. */
|
|
15121
|
-
const MAX_SKILL_DEPTH = 4;
|
|
15122
|
-
/**
|
|
15123
|
-
* Entries that are never part of a skill, skipped rather than refused.
|
|
15124
|
-
*
|
|
15125
|
-
* The distinction matters. Refusing an asset because it cannot be represented
|
|
15126
|
-
* faithfully is right — a half-materialized skill loads and misbehaves. But a
|
|
15127
|
-
* `.DS_Store` is not part of the skill at all, and macOS drops one into any
|
|
15128
|
-
* directory that has been opened in Finder, so refusing on its account rejects
|
|
15129
|
-
* a perfectly good skill for a reason its author cannot see.
|
|
15130
|
-
*
|
|
15131
|
-
* These could not sync in any case: `AgentAssetFilePath` rejects a leading dot,
|
|
15132
|
-
* so a dotfile has no representable path even if it were text.
|
|
15133
|
-
*/
|
|
15134
|
-
const IGNORED_ENTRIES = /* @__PURE__ */ new Set([
|
|
15135
|
-
".DS_Store",
|
|
15136
|
-
"Thumbs.db",
|
|
15137
|
-
".git",
|
|
15138
|
-
".svn",
|
|
15139
|
-
".hg",
|
|
15140
|
-
"node_modules",
|
|
15141
|
-
"__pycache__",
|
|
15142
|
-
".venv",
|
|
15143
|
-
".pytest_cache",
|
|
15144
|
-
".ruff_cache",
|
|
15145
|
-
".mypy_cache"
|
|
15146
|
-
]);
|
|
15147
|
-
/**
|
|
15148
|
-
* Anything else with a leading dot is skipped too — an editor's `.idea`, a
|
|
15149
|
-
* `.gitignore` that belongs to the checkout rather than the skill. The rule is
|
|
15150
|
-
* the same one the path schema already enforces, applied earlier so it reads as
|
|
15151
|
-
* "not part of the skill" instead of "this skill is broken".
|
|
15152
|
-
*/
|
|
15153
|
-
const isIgnoredEntry = (entry) => IGNORED_ENTRIES.has(entry) || entry.startsWith(".");
|
|
15154
|
-
/**
|
|
15155
|
-
* A file is binary if decoding it yields a NUL.
|
|
15156
|
-
*
|
|
15157
|
-
* Cheap, and right for what is being asked: the question is not "is this valid
|
|
15158
|
-
* UTF-8" but "can this round-trip through a JSON text column", and a NUL is
|
|
15159
|
-
* what actually breaks that on the way back out of SQLite.
|
|
15160
|
-
*/
|
|
15161
|
-
const looksBinary = (content) => content.includes("\0");
|
|
15162
|
-
const readSkillFiles = Effect.fn("skillDirectory.readSkillFiles")(function* (root) {
|
|
15163
|
-
const fileSystem = yield* FileSystem.FileSystem;
|
|
15164
|
-
const path = yield* Path.Path;
|
|
15165
|
-
const files = [];
|
|
15166
|
-
const walk = Effect.fn("skillDirectory.walk")(function* (directory, prefix, depth) {
|
|
15167
|
-
if (depth > MAX_SKILL_DEPTH) return "too-deep";
|
|
15168
|
-
const entries = yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []));
|
|
15169
|
-
for (const entry of [...entries].sort()) {
|
|
15170
|
-
if (isIgnoredEntry(entry)) continue;
|
|
15171
|
-
const absolute = path.join(directory, entry);
|
|
15172
|
-
const relative = prefix === "" ? entry : `${prefix}/${entry}`;
|
|
15173
|
-
const info = yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0));
|
|
15174
|
-
if (info === void 0) return "unreadable";
|
|
15175
|
-
if (info.type === "Directory") {
|
|
15176
|
-
const nested = yield* walk(absolute, relative, depth + 1);
|
|
15177
|
-
if (nested !== null) return nested;
|
|
15178
|
-
continue;
|
|
15179
|
-
}
|
|
15180
|
-
if (info.type !== "File") continue;
|
|
15181
|
-
if (files.length >= 512) return "too-many-files";
|
|
15182
|
-
const content = yield* fileSystem.readFileString(absolute).pipe(Effect.orElseSucceed(() => void 0));
|
|
15183
|
-
if (content === void 0) return "unreadable";
|
|
15184
|
-
if (looksBinary(content)) return "binary-file";
|
|
15185
|
-
if (content.length > 524288) return "file-too-large";
|
|
15186
|
-
files.push({
|
|
15187
|
-
path: relative,
|
|
15188
|
-
content
|
|
15189
|
-
});
|
|
15190
|
-
}
|
|
15191
|
-
return null;
|
|
15192
|
-
});
|
|
15193
|
-
const refusal = yield* walk(root, "", 1);
|
|
15194
|
-
if (refusal !== null) return { refusal };
|
|
15195
|
-
if (agentAssetTotalBytes(files) > 4194304) return { refusal: "asset-too-large" };
|
|
15196
|
-
return { files };
|
|
15197
|
-
});
|
|
15198
|
-
/**
|
|
15199
|
-
* Read every skill directory under a user-scope `skills/` root.
|
|
15200
|
-
*
|
|
15201
|
-
* A directory with no `SKILL.md` is not a skill and is skipped silently —
|
|
15202
|
-
* Claude Code would ignore it too, so reporting it would be noise about
|
|
15203
|
-
* something that was never a skill in the first place.
|
|
15204
|
-
*/
|
|
15205
|
-
const readSkillDirectory = Effect.fn("skillDirectory.readSkillDirectory")(function* (skillsRoot) {
|
|
15206
|
-
const fileSystem = yield* FileSystem.FileSystem;
|
|
15207
|
-
const path = yield* Path.Path;
|
|
15208
|
-
const entries = yield* fileSystem.readDirectory(skillsRoot).pipe(Effect.orElseSucceed(() => []));
|
|
15209
|
-
const skills = [];
|
|
15210
|
-
const refused = [];
|
|
15211
|
-
for (const entry of [...entries].sort()) {
|
|
15212
|
-
if (isIgnoredEntry(entry)) continue;
|
|
15213
|
-
const directory = path.join(skillsRoot, entry);
|
|
15214
|
-
if ((yield* fileSystem.stat(directory).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "Directory") continue;
|
|
15215
|
-
if (!(yield* fileSystem.exists(path.join(directory, "SKILL.md")).pipe(Effect.orElseSucceed(() => false)))) continue;
|
|
15216
|
-
const read = yield* readSkillFiles(directory);
|
|
15217
|
-
if ("refusal" in read) {
|
|
15218
|
-
refused.push({
|
|
15219
|
-
name: entry,
|
|
15220
|
-
reason: read.refusal
|
|
15221
|
-
});
|
|
15222
|
-
continue;
|
|
15223
|
-
}
|
|
15224
|
-
skills.push({
|
|
15225
|
-
name: entry,
|
|
15226
|
-
files: read.files
|
|
15227
|
-
});
|
|
15228
|
-
}
|
|
15229
|
-
return {
|
|
15230
|
-
skills,
|
|
15231
|
-
refused
|
|
15232
|
-
};
|
|
15233
|
-
});
|
|
15234
|
-
/**
|
|
15235
|
-
* Replace a skill directory with exactly the asset's files.
|
|
15236
|
-
*
|
|
15237
|
-
* Written to a sibling and then swapped, so a crash mid-write leaves the old
|
|
15238
|
-
* skill intact rather than half of the new one. A skill is loaded as a unit and
|
|
15239
|
-
* a partially written one is instructions with the middle missing, which an
|
|
15240
|
-
* agent will happily follow.
|
|
15241
|
-
*/
|
|
15242
|
-
const writeSkillDirectory = Effect.fn("skillDirectory.writeSkillDirectory")(function* (input) {
|
|
15243
|
-
const fileSystem = yield* FileSystem.FileSystem;
|
|
15244
|
-
const path = yield* Path.Path;
|
|
15245
|
-
const target = path.join(input.skillsRoot, input.name);
|
|
15246
|
-
const staging = path.join(input.skillsRoot, `.${input.name}.p4code-staging`);
|
|
15247
|
-
yield* fileSystem.makeDirectory(input.skillsRoot, { recursive: true });
|
|
15248
|
-
yield* fileSystem.remove(staging, {
|
|
15249
|
-
recursive: true,
|
|
15250
|
-
force: true
|
|
15251
|
-
});
|
|
15252
|
-
yield* fileSystem.makeDirectory(staging, { recursive: true });
|
|
15253
|
-
for (const file of input.files) {
|
|
15254
|
-
const destination = path.join(staging, file.path);
|
|
15255
|
-
yield* fileSystem.makeDirectory(path.dirname(destination), { recursive: true });
|
|
15256
|
-
yield* fileSystem.writeFileString(destination, file.content);
|
|
15257
|
-
}
|
|
15258
|
-
yield* fileSystem.remove(target, {
|
|
15259
|
-
recursive: true,
|
|
15260
|
-
force: true
|
|
15261
|
-
});
|
|
15262
|
-
yield* fileSystem.rename(staging, target);
|
|
15263
|
-
});
|
|
15264
|
-
const removeSkillDirectory = Effect.fn("skillDirectory.removeSkillDirectory")(function* (input) {
|
|
15265
|
-
const fileSystem = yield* FileSystem.FileSystem;
|
|
15266
|
-
const path = yield* Path.Path;
|
|
15267
|
-
yield* fileSystem.remove(path.join(input.skillsRoot, input.name), {
|
|
15268
|
-
recursive: true,
|
|
15269
|
-
force: true
|
|
15270
|
-
});
|
|
15271
|
-
});
|
|
15272
|
-
//#endregion
|
|
15273
15346
|
//#region src/sync/skillSyncPlan.ts
|
|
15274
15347
|
/**
|
|
15275
15348
|
* Deciding what a skill sync should do, given three views of the world.
|
|
@@ -16211,7 +16284,7 @@ const makeAssetCommands = (words) => {
|
|
|
16211
16284
|
name: nameArgument,
|
|
16212
16285
|
yes: Flag.boolean("yes").pipe(Flag.withDescription("Confirm: this removes it from the hub and from every machine."))
|
|
16213
16286
|
}).pipe(Command.withDescription(`Delete a ${words.singular} from the hub and from every machine.`), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => Effect.gen(function* () {
|
|
16214
|
-
if (!flags.yes) return yield* Console.error(`This deletes ${flags.name} from the hub and from every machine that syncs it.\nRe-run with --yes if that is what you want.\nTo stop sharing it from here only:
|
|
16287
|
+
if (!flags.yes) return yield* Console.error(`This deletes ${flags.name} from the hub and from every machine that syncs it.\nRe-run with --yes if that is what you want.\nTo stop sharing it from here only: p4c ${words.command} unpublish ${flags.name}`);
|
|
16215
16288
|
const report = yield* sync.deleteFromHub({
|
|
16216
16289
|
kind: words.kind,
|
|
16217
16290
|
name: flags.name
|
|
@@ -24226,6 +24299,7 @@ const make$51 = Effect.fn("cloud.boot_service.make")(function* (input) {
|
|
|
24226
24299
|
});
|
|
24227
24300
|
const activateLaunchAgent = Effect.gen(function* () {
|
|
24228
24301
|
yield* runStep("unloading the previous service", "launchctl", ["bootout", serviceTarget]).pipe(Effect.ignore);
|
|
24302
|
+
yield* runStep("enabling the service", "launchctl", ["enable", serviceTarget]).pipe(Effect.ignore);
|
|
24229
24303
|
yield* runStep("loading the service", "launchctl", [
|
|
24230
24304
|
"bootstrap",
|
|
24231
24305
|
guiDomain,
|
|
@@ -44371,14 +44445,15 @@ const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(functio
|
|
|
44371
44445
|
const versionProbe = yield* runClaudeCommand(claudeSettings, ["--version"], resolvedEnvironment).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result);
|
|
44372
44446
|
if (Result.isFailure(versionProbe)) {
|
|
44373
44447
|
const error = versionProbe.failure;
|
|
44374
|
-
|
|
44448
|
+
const missing = isCommandMissingCause(error);
|
|
44449
|
+
yield* (missing ? Effect.logDebug : Effect.logWarning)("Claude Agent CLI health check failed.", { errorTag: error._tag });
|
|
44375
44450
|
return buildServerProvider({
|
|
44376
44451
|
presentation: CLAUDE_PRESENTATION,
|
|
44377
44452
|
enabled: claudeSettings.enabled,
|
|
44378
44453
|
checkedAt,
|
|
44379
44454
|
models: allModels,
|
|
44380
44455
|
probe: {
|
|
44381
|
-
installed: !
|
|
44456
|
+
installed: !missing,
|
|
44382
44457
|
version: null,
|
|
44383
44458
|
status: "error",
|
|
44384
44459
|
auth: { status: "unknown" },
|
|
@@ -73561,14 +73636,15 @@ const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(functio
|
|
|
73561
73636
|
const aboutProbe = yield* runCursorAboutCommand(cursorSettings, environment).pipe(Effect.timeoutOption(ABOUT_TIMEOUT_MS), Effect.result);
|
|
73562
73637
|
if (Result.isFailure(aboutProbe)) {
|
|
73563
73638
|
const error = aboutProbe.failure;
|
|
73564
|
-
|
|
73639
|
+
const missing = isCommandMissingCause(error);
|
|
73640
|
+
yield* (missing ? Effect.logDebug : Effect.logWarning)("Cursor Agent CLI health check failed.", { errorTag: error._tag });
|
|
73565
73641
|
return buildServerProvider({
|
|
73566
73642
|
presentation: CURSOR_PRESENTATION,
|
|
73567
73643
|
enabled: cursorSettings.enabled,
|
|
73568
73644
|
checkedAt,
|
|
73569
73645
|
models: fallbackModels,
|
|
73570
73646
|
probe: {
|
|
73571
|
-
installed: !
|
|
73647
|
+
installed: !missing,
|
|
73572
73648
|
version: null,
|
|
73573
73649
|
status: "error",
|
|
73574
73650
|
auth: { status: "unknown" },
|
|
@@ -76205,14 +76281,15 @@ const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(function* (
|
|
|
76205
76281
|
const versionResult = yield* runGrokVersionCommand(grokSettings, environment).pipe(Effect.timeoutOption(VERSION_PROBE_TIMEOUT_MS), Effect.result);
|
|
76206
76282
|
if (Result.isFailure(versionResult)) {
|
|
76207
76283
|
const error = versionResult.failure;
|
|
76208
|
-
|
|
76284
|
+
const missing = isCommandMissingCause(error);
|
|
76285
|
+
yield* (missing ? Effect.logDebug : Effect.logWarning)("Grok CLI health check failed.", { errorTag: error._tag });
|
|
76209
76286
|
return buildServerProvider({
|
|
76210
76287
|
presentation: GROK_PRESENTATION,
|
|
76211
76288
|
enabled: grokSettings.enabled,
|
|
76212
76289
|
checkedAt,
|
|
76213
76290
|
models: fallbackModels,
|
|
76214
76291
|
probe: {
|
|
76215
|
-
installed: !
|
|
76292
|
+
installed: !missing,
|
|
76216
76293
|
version: null,
|
|
76217
76294
|
status: "error",
|
|
76218
76295
|
auth: { status: "unknown" },
|
|
@@ -82690,7 +82767,37 @@ const makeCli = () => Command.make("p4c", { ...sharedServerCommandFlags }).pipe(
|
|
|
82690
82767
|
skillsCommand
|
|
82691
82768
|
]));
|
|
82692
82769
|
const cli = makeCli();
|
|
82693
|
-
|
|
82770
|
+
/**
|
|
82771
|
+
* A one-line message for an expected failure, without the stack.
|
|
82772
|
+
*
|
|
82773
|
+
* Every domain error here is a `Schema.TaggedErrorClass` with a `message`
|
|
82774
|
+
* getter written for a person — "Background setup failed while loading the
|
|
82775
|
+
* service (exit code 5)" — and the surrounding command has usually already
|
|
82776
|
+
* printed the log path that holds the detail. Appending twenty frames of
|
|
82777
|
+
* bundled `dist/bin.mjs` to that adds nothing anybody can act on.
|
|
82778
|
+
*/
|
|
82779
|
+
const describeExpectedFailure = (error) => {
|
|
82780
|
+
const message = error?.message;
|
|
82781
|
+
if (typeof message === "string" && message.trim().length > 0) return message;
|
|
82782
|
+
return String(error);
|
|
82783
|
+
};
|
|
82784
|
+
/**
|
|
82785
|
+
* Expected failures print their message; defects keep their stack.
|
|
82786
|
+
*
|
|
82787
|
+
* The distinction is Effect's own, and it is the right one: a typed failure is
|
|
82788
|
+
* a condition the code anticipated and described, so a trace through a
|
|
82789
|
+
* bundled file is noise. A defect is a bug, and there the trace is the only
|
|
82790
|
+
* thing worth having.
|
|
82791
|
+
*
|
|
82792
|
+
* `CliError` is re-raised rather than handled, because `runMain` already
|
|
82793
|
+
* renders usage and validation failures better than a bare message would —
|
|
82794
|
+
* catching them here would replace "unknown flag --foo, did you mean --force"
|
|
82795
|
+
* with one flat line.
|
|
82796
|
+
*/
|
|
82797
|
+
const reportFailuresWithoutStacks = (effect) => effect.pipe(Effect.catchIf((error) => !CliError.isCliError(error), (error) => Console.error(describeExpectedFailure(error)).pipe(Effect.andThen(Effect.sync(() => {
|
|
82798
|
+
process.exitCode = 1;
|
|
82799
|
+
})))));
|
|
82800
|
+
if (import.meta.main) reportUpdateAvailable.pipe(Effect.andThen(Command.run(cli, { version })), reportFailuresWithoutStacks, Effect.scoped, Effect.provide(CliRuntimeLayer), NodeRuntime.runMain);
|
|
82694
82801
|
//#endregion
|
|
82695
82802
|
export { cli, makeCli };
|
|
82696
82803
|
|