@p4code/cli 0.0.49 → 0.0.50

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 CHANGED
@@ -55,6 +55,7 @@ import * as Exit from "effect/Exit";
55
55
  import * as SchemaGetter from "effect/SchemaGetter";
56
56
  import * as Config from "effect/Config";
57
57
  import * as NodeReadline from "node:readline";
58
+ import * as Clock from "effect/Clock";
58
59
  import * as Cache from "effect/Cache";
59
60
  import * as Deferred from "effect/Deferred";
60
61
  import * as Equal from "effect/Equal";
@@ -62,7 +63,6 @@ import * as Semaphore from "effect/Semaphore";
62
63
  import * as Scope from "effect/Scope";
63
64
  import { parse } from "yaml";
64
65
  import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
65
- import * as Clock from "effect/Clock";
66
66
  import * as Metric from "effect/Metric";
67
67
  import * as Queue from "effect/Queue";
68
68
  import { cast, dual } from "effect/Function";
@@ -121,7 +121,7 @@ const closeServer = (server) => {
121
121
  * NetService - Service tag for startup networking helpers.
122
122
  */
123
123
  var NetService = class extends Context.Service()("@p4code/shared/Net/NetService") {};
124
- const make$71 = () => {
124
+ const make$72 = () => {
125
125
  /**
126
126
  * Returns true when a TCP server can bind to {host, port}.
127
127
  * `EADDRNOTAVAIL` is treated as available so IPv6-absent hosts don't fail
@@ -230,10 +230,10 @@ const make$71 = () => {
230
230
  })
231
231
  };
232
232
  };
233
- const layer$67 = Layer.sync(NetService, make$71);
233
+ const layer$68 = Layer.sync(NetService, make$72);
234
234
  //#endregion
235
235
  //#region package.json
236
- var version = "0.0.49";
236
+ var version = "0.0.50";
237
237
  //#endregion
238
238
  //#region src/config.ts
239
239
  /**
@@ -254,8 +254,8 @@ var ServerConfig$1 = class extends Context.Service()("@p4code/cli/config/ServerC
254
254
  /** @deprecated Import and use `layerTest` from this module. */
255
255
  static layerTest = (cwd, baseDirOrPrefix) => layerTest$2(cwd, baseDirOrPrefix);
256
256
  };
257
- const make$70 = (config) => ServerConfig$1.of(config);
258
- const layer$66 = (config) => Layer.succeed(ServerConfig$1, make$70(config));
257
+ const make$71 = (config) => ServerConfig$1.of(config);
258
+ const layer$67 = (config) => Layer.succeed(ServerConfig$1, make$71(config));
259
259
  const deriveServerPaths = Effect.fn(function* (baseDir, devUrl, options = {}) {
260
260
  const { join } = yield* Path.Path;
261
261
  const stateDir = join(baseDir, devUrl !== void 0 && !options.baseDirIsExplicit ? "dev" : "userdata");
@@ -869,7 +869,7 @@ function compareSemverVersions(left, right) {
869
869
  * timeout, every failure swallowed, and never anything that changes an exit
870
870
  * code. A version check that can break a command is worse than no check.
871
871
  */
872
- const PACKAGE_NAME = "@p4code/cli";
872
+ const PACKAGE_NAME$1 = "@p4code/cli";
873
873
  const CHECK_INTERVAL = Duration.hours(24);
874
874
  const REQUEST_TIMEOUT = Duration.seconds(3);
875
875
  /**
@@ -889,7 +889,7 @@ const encodeCacheEntry = Schema$1.encodeEffect(CacheEntryJson);
889
889
  const NpmLatest = Schema$1.Struct({ version: Schema$1.optional(Schema$1.String) });
890
890
  const fetchLatestVersion = Effect.fn("cli.updateCheck.fetch")(function* () {
891
891
  const client = yield* HttpClient.HttpClient;
892
- const request = HttpClientRequest.get(`https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`).pipe(HttpClientRequest.setHeader("accept", "application/json"));
892
+ const request = HttpClientRequest.get(`https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME$1)}/latest`).pipe(HttpClientRequest.setHeader("accept", "application/json"));
893
893
  const response = yield* client.execute(request).pipe(Effect.timeout(REQUEST_TIMEOUT));
894
894
  if (response.status < 200 || response.status >= 300) return null;
895
895
  const version = (yield* response.json.pipe(Effect.flatMap(Schema$1.decodeUnknownEffect(NpmLatest)))).version?.trim();
@@ -923,7 +923,7 @@ const resolveLatestVersion = Effect.fn("cli.updateCheck.resolve")(function* (sta
923
923
  * `service update` restarts it onto the new one.
924
924
  */
925
925
  function formatUpdateReminder(input) {
926
- return [`Update available: ${PACKAGE_NAME}@${input.latestVersion} (running ${input.currentVersion}).`, ` npm i -g ${PACKAGE_NAME}@latest && p4c service update`].join("\n");
926
+ return [`Update available: ${PACKAGE_NAME$1}@${input.latestVersion} (running ${input.currentVersion}).`, ` npm i -g ${PACKAGE_NAME$1}@latest && p4c service update`].join("\n");
927
927
  }
928
928
  /**
929
929
  * The reminder line, or null when there is nothing to say. Callers print it;
@@ -8736,7 +8736,7 @@ function deriveAuthClientMetadata(input) {
8736
8736
  //#endregion
8737
8737
  //#region src/auth/EnvironmentAuthPolicy.ts
8738
8738
  var EnvironmentAuthPolicy = class extends Context.Service()("@p4code/cli/auth/EnvironmentAuthPolicy") {};
8739
- const make$69 = Effect.gen(function* () {
8739
+ const make$70 = Effect.gen(function* () {
8740
8740
  const config = yield* ServerConfig$1;
8741
8741
  const isRemoteReachable = isRemoteReachableHost(config.host);
8742
8742
  const policy = config.mode === "desktop" ? isRemoteReachable ? "remote-reachable" : "desktop-managed-local" : isRemoteReachable ? "remote-reachable" : "loopback-browser";
@@ -8754,7 +8754,7 @@ const make$69 = Effect.gen(function* () {
8754
8754
  };
8755
8755
  return EnvironmentAuthPolicy.of({ getDescriptor: () => Effect.succeed(descriptor).pipe(Effect.withSpan("EnvironmentAuthPolicy.getDescriptor")) });
8756
8756
  });
8757
- const layer$65 = Layer.effect(EnvironmentAuthPolicy, make$69);
8757
+ const layer$66 = Layer.effect(EnvironmentAuthPolicy, make$70);
8758
8758
  //#endregion
8759
8759
  //#region src/persistence/Errors.ts
8760
8760
  function summarizeSchemaIssue(issue) {
@@ -8935,7 +8935,7 @@ function toPersistenceSqlOrDecodeError$6(sqlOperation, decodeOperation, correlat
8935
8935
  cause
8936
8936
  });
8937
8937
  }
8938
- const make$68 = Effect.gen(function* () {
8938
+ const make$69 = Effect.gen(function* () {
8939
8939
  const sql = yield* SqlClient.SqlClient;
8940
8940
  const createSessionRow = SqlSchema.void({
8941
8941
  Request: CreateAuthSessionInput,
@@ -9069,7 +9069,7 @@ const make$68 = Effect.gen(function* () {
9069
9069
  setLastConnectedAt
9070
9070
  };
9071
9071
  });
9072
- const layer$64 = Layer.effect(AuthSessionRepository, make$68);
9072
+ const layer$65 = Layer.effect(AuthSessionRepository, make$69);
9073
9073
  //#endregion
9074
9074
  //#region src/auth/ServerSecretStore.ts
9075
9075
  const secretStoreErrorContext = {
@@ -9136,7 +9136,7 @@ const isSecretStoreError = Schema$1.is(SecretStoreError);
9136
9136
  const isPlatformError = (value) => Predicate.isTagged(value, "PlatformError");
9137
9137
  const isSecretAlreadyExistsError = (error) => "cause" in error && isPlatformError(error.cause) && error.cause.reason._tag === "AlreadyExists";
9138
9138
  var ServerSecretStore = class extends Context.Service()("@p4code/cli/auth/ServerSecretStore") {};
9139
- const make$67 = Effect.gen(function* () {
9139
+ const make$68 = Effect.gen(function* () {
9140
9140
  const crypto = yield* Crypto.Crypto;
9141
9141
  const fileSystem = yield* FileSystem.FileSystem;
9142
9142
  const path = yield* Path.Path;
@@ -9206,7 +9206,7 @@ const make$67 = Effect.gen(function* () {
9206
9206
  remove
9207
9207
  });
9208
9208
  });
9209
- const layer$63 = Layer.effect(ServerSecretStore, make$67);
9209
+ const layer$64 = Layer.effect(ServerSecretStore, make$68);
9210
9210
  //#endregion
9211
9211
  //#region src/auth/SessionStore.ts
9212
9212
  var MalformedSessionTokenError = class extends Schema$1.TaggedErrorClass()("MalformedSessionTokenError", {}) {
@@ -9444,7 +9444,7 @@ function toAuthClientSession(input) {
9444
9444
  current: false
9445
9445
  };
9446
9446
  }
9447
- const make$66 = Effect.gen(function* () {
9447
+ const make$67 = Effect.gen(function* () {
9448
9448
  const crypto = yield* Crypto.Crypto;
9449
9449
  const serverConfig = yield* ServerConfig$1;
9450
9450
  const secretStore = yield* ServerSecretStore;
@@ -9758,7 +9758,7 @@ const make$66 = Effect.gen(function* () {
9758
9758
  markDisconnected
9759
9759
  });
9760
9760
  });
9761
- const layer$62 = Layer.effect(SessionStore, make$66).pipe(Layer.provideMerge(layer$64));
9761
+ const layer$63 = Layer.effect(SessionStore, make$67).pipe(Layer.provideMerge(layer$65));
9762
9762
  //#endregion
9763
9763
  //#region src/persistence/AuthPairingLinks.ts
9764
9764
  const AuthPairingLinkRecord = Schema$1.Struct({
@@ -9819,7 +9819,7 @@ function toPersistenceSqlOrDecodeError$5(sqlOperation, decodeOperation, correlat
9819
9819
  cause
9820
9820
  });
9821
9821
  }
9822
- const make$65 = Effect.gen(function* () {
9822
+ const make$66 = Effect.gen(function* () {
9823
9823
  const sql = yield* SqlClient.SqlClient;
9824
9824
  const createPairingLinkRow = SqlSchema.void({
9825
9825
  Request: CreateAuthPairingLinkInput,
@@ -9954,7 +9954,7 @@ const make$65 = Effect.gen(function* () {
9954
9954
  getByCredential
9955
9955
  };
9956
9956
  });
9957
- const layer$61 = Layer.effect(AuthPairingLinkRepository, make$65);
9957
+ const layer$62 = Layer.effect(AuthPairingLinkRepository, make$66);
9958
9958
  //#endregion
9959
9959
  //#region src/auth/PairingGrantStore.ts
9960
9960
  var UnknownBootstrapCredentialError = class extends Schema$1.TaggedErrorClass()("UnknownBootstrapCredentialError", {}) {
@@ -10049,7 +10049,7 @@ const DEV_STARTUP_TTL_HOURS = Duration.hours(24);
10049
10049
  const PAIRING_TOKEN_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
10050
10050
  const PAIRING_TOKEN_LENGTH = 12;
10051
10051
  const PAIRING_TOKEN_REJECTION_LIMIT = Math.floor(256 / 32) * 32;
10052
- const make$64 = Effect.gen(function* () {
10052
+ const make$65 = Effect.gen(function* () {
10053
10053
  const crypto = yield* Crypto.Crypto;
10054
10054
  const config = yield* ServerConfig$1;
10055
10055
  const pairingLinks = yield* AuthPairingLinkRepository;
@@ -10247,7 +10247,7 @@ const make$64 = Effect.gen(function* () {
10247
10247
  consume
10248
10248
  });
10249
10249
  });
10250
- const layer$60 = Layer.effect(PairingGrantStore, make$64).pipe(Layer.provideMerge(layer$61));
10250
+ const layer$61 = Layer.effect(PairingGrantStore, make$65).pipe(Layer.provideMerge(layer$62));
10251
10251
  //#endregion
10252
10252
  //#region src/persistence/Migrations/001_OrchestrationEvents.ts
10253
10253
  var _001_OrchestrationEvents_default = Effect.gen(function* () {
@@ -12020,7 +12020,7 @@ function parseBearerToken(request) {
12020
12020
  const token = header.slice(7).trim();
12021
12021
  return token.length > 0 ? token : null;
12022
12022
  }
12023
- const make$63 = Effect.gen(function* () {
12023
+ const make$64 = Effect.gen(function* () {
12024
12024
  const policy = yield* EnvironmentAuthPolicy;
12025
12025
  const bootstrapCredentials = yield* PairingGrantStore;
12026
12026
  const sessions = yield* SessionStore;
@@ -12215,9 +12215,9 @@ const make$63 = Effect.gen(function* () {
12215
12215
  issueStartupPairingUrl
12216
12216
  });
12217
12217
  });
12218
- const layer$59 = Layer.effect(EnvironmentAuth, make$63).pipe(Layer.provideMerge(layer$60), Layer.provideMerge(layer$62), Layer.provideMerge(layer$65));
12219
- const storageLayer = Layer.mergeAll(layer$63, layerConfig);
12220
- const runtimeLayer = layer$59.pipe(Layer.provideMerge(storageLayer));
12218
+ const layer$60 = Layer.effect(EnvironmentAuth, make$64).pipe(Layer.provideMerge(layer$61), Layer.provideMerge(layer$63), Layer.provideMerge(layer$66));
12219
+ const storageLayer = Layer.mergeAll(layer$64, layerConfig);
12220
+ const runtimeLayer = layer$60.pipe(Layer.provideMerge(storageLayer));
12221
12221
  //#endregion
12222
12222
  //#region src/cliAuthFormat.ts
12223
12223
  const newline = "\n";
@@ -12972,7 +12972,7 @@ const runWithEnvironmentAuth = (flags, run, options) => Effect.gen(function* ()
12972
12972
  }
12973
12973
  return yield* Effect.gen(function* () {
12974
12974
  return yield* run(yield* EnvironmentAuth);
12975
- }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer).pipe(Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
12975
+ }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer).pipe(Layer.provide(layer$67(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
12976
12976
  });
12977
12977
  const ttlFlag = Flag.string("ttl").pipe(Flag.withSchema(DurationFromString), Flag.withDescription("TTL, for example `5m`, `1h`, `30d`, or `15 minutes`."), Flag.optional);
12978
12978
  const jsonFlag = Flag.boolean("json").pipe(Flag.withDescription("Emit JSON instead of human-readable output."), Flag.withDefault(false));
@@ -13069,6 +13069,64 @@ const authCommand = Command.make("auth").pipe(Command.withDescription("Manage th
13069
13069
  * @module HubConfig
13070
13070
  */
13071
13071
  var HubConfig = class extends Context.Service()("@p4code/cli/hub/HubConfig") {};
13072
+ const FAILURE_WINDOW_MS = 6e4;
13073
+ const BLOCK_DURATION_MS = 6e4;
13074
+ const initialThrottleState = {
13075
+ windowStartMillis: 0,
13076
+ failures: 0,
13077
+ blockedUntilMillis: 0
13078
+ };
13079
+ /** Whether a *failing* request should be refused outright rather than checked. */
13080
+ const isBlocked = (state, nowMillis) => nowMillis < state.blockedUntilMillis;
13081
+ /** Seconds a caller should wait, for `retry-after`. Always at least one. */
13082
+ const retryAfterSeconds = (state, nowMillis) => Math.max(1, Math.ceil((state.blockedUntilMillis - nowMillis) / 1e3));
13083
+ /**
13084
+ * Count a failure, and start blocking once the window fills.
13085
+ *
13086
+ * The window is fixed rather than sliding: a sliding window needs the timestamp
13087
+ * of every attempt, which is unbounded memory for an unbounded attacker. A
13088
+ * fixed window lets a burst straddle a boundary and land up to twice the limit,
13089
+ * which for this purpose is a rounding error against the size of the search
13090
+ * space.
13091
+ */
13092
+ const recordFailure = (state, nowMillis, limits = {
13093
+ maxFailures: 10,
13094
+ windowMillis: FAILURE_WINDOW_MS,
13095
+ blockMillis: BLOCK_DURATION_MS
13096
+ }) => {
13097
+ if (nowMillis - state.windowStartMillis >= limits.windowMillis) return {
13098
+ windowStartMillis: nowMillis,
13099
+ failures: 1,
13100
+ blockedUntilMillis: 0
13101
+ };
13102
+ const failures = state.failures + 1;
13103
+ return {
13104
+ windowStartMillis: state.windowStartMillis,
13105
+ failures,
13106
+ blockedUntilMillis: failures >= limits.maxFailures ? nowMillis + limits.blockMillis : state.blockedUntilMillis
13107
+ };
13108
+ };
13109
+ var HubAuthThrottle = class extends Context.Service()("@p4code/cli/hub/HubAuthThrottle") {};
13110
+ const DEFAULT_LIMITS = {
13111
+ maxFailures: 10,
13112
+ windowMillis: FAILURE_WINDOW_MS,
13113
+ blockMillis: BLOCK_DURATION_MS
13114
+ };
13115
+ const make$63 = Effect.fn("HubAuthThrottle.make")(function* (limits = DEFAULT_LIMITS) {
13116
+ const state = yield* Ref.make(initialThrottleState);
13117
+ return HubAuthThrottle.of({
13118
+ shouldRefuse: Effect.gen(function* () {
13119
+ const now = yield* Clock.currentTimeMillis;
13120
+ return isBlocked(yield* Ref.get(state), now);
13121
+ }),
13122
+ recordFailure: Effect.gen(function* () {
13123
+ const now = yield* Clock.currentTimeMillis;
13124
+ const next = yield* Ref.updateAndGet(state, (current) => recordFailure(current, now, limits));
13125
+ return retryAfterSeconds(next, now);
13126
+ })
13127
+ });
13128
+ });
13129
+ const layer$59 = Layer.effect(HubAuthThrottle, make$63());
13072
13130
  //#endregion
13073
13131
  //#region src/hub/HubAuth.ts
13074
13132
  /**
@@ -13127,6 +13185,31 @@ var HubUnauthorizedError = class {
13127
13185
  _tag = "HubUnauthorizedError";
13128
13186
  };
13129
13187
  /**
13188
+ * Too many failed attempts recently; this one was not even checked.
13189
+ *
13190
+ * Distinct from `HubUnauthorizedError` so the response can differ. A 429 is the
13191
+ * honest answer — the credential was not evaluated, so calling it invalid would
13192
+ * be a claim the hub did not make — and it tells a legitimate server retrying a
13193
+ * stale token to back off rather than hammer.
13194
+ */
13195
+ var HubThrottledError = class {
13196
+ _tag = "HubThrottledError";
13197
+ retryAfterSeconds;
13198
+ constructor(retryAfterSeconds) {
13199
+ this.retryAfterSeconds = retryAfterSeconds;
13200
+ }
13201
+ };
13202
+ const hubThrottledResponse = (retryAfterSeconds) => HttpServerResponse.jsonUnsafe({
13203
+ error: "too_many_hub_credential_failures",
13204
+ message: "Too many failed credential attempts. Try again shortly."
13205
+ }, {
13206
+ status: 429,
13207
+ headers: {
13208
+ "cache-control": "no-store",
13209
+ "retry-after": String(retryAfterSeconds)
13210
+ }
13211
+ });
13212
+ /**
13130
13213
  * Fails with {@link HubUnauthorizedError} unless the request carries a
13131
13214
  * configured bearer token. Route handlers run this first and let
13132
13215
  * `respondToHubAuthFailure` turn the failure into a 401.
@@ -13134,11 +13217,17 @@ var HubUnauthorizedError = class {
13134
13217
  const authenticateHubRequest = Effect.fn("HubAuth.authenticateHubRequest")(function* () {
13135
13218
  const request = yield* HttpServerRequest.HttpServerRequest;
13136
13219
  const { tokenHashes } = yield* HubConfig;
13220
+ const throttle = yield* HubAuthThrottle;
13137
13221
  const token = readBearerToken(request.headers.authorization);
13138
- if (!isAuthorizedHubToken(token, tokenHashes)) {
13139
- yield* Effect.logWarning("rejected hub request with an unusable credential", { reason: token.length === 0 ? "missing_bearer_token" : "unknown_token" });
13140
- return yield* Effect.fail(new HubUnauthorizedError());
13222
+ if (isAuthorizedHubToken(token, tokenHashes)) return;
13223
+ if (yield* throttle.shouldRefuse) {
13224
+ const retryAfter = yield* throttle.recordFailure;
13225
+ yield* Effect.logWarning("refused hub request without checking; too many recent failures", { reason: token.length === 0 ? "missing_bearer_token" : "unknown_token" });
13226
+ return yield* Effect.fail(new HubThrottledError(retryAfter));
13141
13227
  }
13228
+ yield* throttle.recordFailure;
13229
+ yield* Effect.logWarning("rejected hub request with an unusable credential", { reason: token.length === 0 ? "missing_bearer_token" : "unknown_token" });
13230
+ return yield* Effect.fail(new HubUnauthorizedError());
13142
13231
  });
13143
13232
  const SHA256_HEX = /^[0-9a-f]{64}$/;
13144
13233
  /**
@@ -13547,6 +13636,7 @@ const storeUnavailable = HttpServerResponse.jsonUnsafe({
13547
13636
  */
13548
13637
  const respondToHubFailures = (handler) => handler.pipe(Effect.catch((cause) => {
13549
13638
  if (cause._tag === "HubUnauthorizedError") return Effect.succeed(hubUnauthorizedResponse);
13639
+ if (cause._tag === "HubThrottledError") return Effect.succeed(hubThrottledResponse(cause.retryAfterSeconds));
13550
13640
  if (cause._tag === "InvalidHubRequest") return Effect.succeed(badRequest(cause.detail));
13551
13641
  return Effect.logWarning("hub request failed", { cause }).pipe(Effect.as(storeUnavailable));
13552
13642
  }));
@@ -13942,7 +14032,7 @@ const announce = Layer.effectDiscard(Effect.gen(function* () {
13942
14032
  });
13943
14033
  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 });
13944
14034
  }));
13945
- const makeHubLayer = HttpRouter.serve(layer$58).pipe(Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
14035
+ const makeHubLayer = HttpRouter.serve(layer$58).pipe(Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(layer$59), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
13946
14036
  const runHub = Layer.launch(makeHubLayer);
13947
14037
  //#endregion
13948
14038
  //#region src/atomicWrite.ts
@@ -16126,7 +16216,7 @@ const make$58 = Effect.gen(function* () {
16126
16216
  const layer$53 = Layer.effect(AssetSync, make$58);
16127
16217
  //#endregion
16128
16218
  //#region src/cli/assets.ts
16129
- const SyncServicesLive = layer$53.pipe(Layer.provideMerge(layer$54), Layer.provideMerge(layer$55), Layer.provideMerge(layer$57.pipe(Layer.provide(layer$63))), Layer.provideMerge(layer$63), Layer.provideMerge(FetchHttpClient.layer));
16219
+ const SyncServicesLive = layer$53.pipe(Layer.provideMerge(layer$54), Layer.provideMerge(layer$55), Layer.provideMerge(layer$57.pipe(Layer.provide(layer$64))), Layer.provideMerge(layer$64), Layer.provideMerge(FetchHttpClient.layer));
16130
16220
  const runWithSync = (flags, run) => Effect.gen(function* () {
16131
16221
  const config = yield* resolveCliAuthConfig(flags, yield* GlobalFlag.LogLevel);
16132
16222
  return yield* Effect.gen(function* () {
@@ -16134,7 +16224,7 @@ const runWithSync = (flags, run) => Effect.gen(function* () {
16134
16224
  sync: yield* AssetSync,
16135
16225
  link: yield* HubLink
16136
16226
  });
16137
- }).pipe(Effect.provide(SyncServicesLive.pipe(Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, "Error")))));
16227
+ }).pipe(Effect.provide(SyncServicesLive.pipe(Layer.provide(layer$67(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, "Error")))));
16138
16228
  });
16139
16229
  const STATE_LABEL = {
16140
16230
  synced: "synced",
@@ -24582,7 +24672,7 @@ const RESTART_DELAY = Duration.seconds(2);
24582
24672
  /** Exact npm versions only — never dist-tags — so the acknowledgement names
24583
24673
  the version that was actually installed. Also keeps the value safe to
24584
24674
  pass to npm and embed in filesystem paths. */
24585
- const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
24675
+ const EXACT_VERSION_PATTERN$1 = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
24586
24676
  function normalizeEntryPath(entryPath) {
24587
24677
  return entryPath.replaceAll("\\", "/");
24588
24678
  }
@@ -24698,7 +24788,7 @@ const make$50 = Effect.fn("cloud.server_self_update.make")(function* (options) {
24698
24788
  if (capability === null) return yield* failWith("This server cannot update itself; relaunch it manually with the new version.");
24699
24789
  const activeMethod = capability;
24700
24790
  const targetVersion = input.targetVersion.trim();
24701
- if (!EXACT_VERSION_PATTERN.test(targetVersion)) return yield* failWith(`'${targetVersion}' is not an exact p4 version.`);
24791
+ if (!EXACT_VERSION_PATTERN$1.test(targetVersion)) return yield* failWith(`'${targetVersion}' is not an exact p4 version.`);
24702
24792
  if (yield* Ref.getAndSet(inFlight, true)) return yield* failWith("A server update is already in progress.");
24703
24793
  return yield* Effect.gen(function* () {
24704
24794
  const runtimePaths = yield* ensurePinnedRuntimeInstalled({
@@ -24727,6 +24817,7 @@ const make$50 = Effect.fn("cloud.server_self_update.make")(function* (options) {
24727
24817
  const homeDir = env.HOME ?? "";
24728
24818
  const unitPath = path.join(homeDir, ".config", "systemd", "user", BOOT_SERVICE_UNIT_FILE);
24729
24819
  const previousUnit = yield* fs.readFileString(unitPath).pipe(Effect.mapError((cause) => failWith("Could not read the current systemd unit.", cause)));
24820
+ const serviceOptions = yield* readServiceOptions({ baseDir: serverConfig.baseDir }).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
24730
24821
  const unit = renderBootServiceUnit({
24731
24822
  nodePath: host.execPath,
24732
24823
  p4EntryPath: runtimePaths.entryPath,
@@ -24734,7 +24825,7 @@ const make$50 = Effect.fn("cloud.server_self_update.make")(function* (options) {
24734
24825
  logPath: path.join(serverConfig.logsDir, "boot-service.log"),
24735
24826
  unitPath,
24736
24827
  workingDirectory: homeDir,
24737
- serviceEnvironment: []
24828
+ serviceEnvironment: serviceEnvironmentFor(serviceOptions)
24738
24829
  });
24739
24830
  yield* writeUnitAtomically(unitPath, unit).pipe(Effect.mapError((cause) => failWith("Could not update the systemd unit.", cause)));
24740
24831
  const reloadSystemd = Effect.fn("cloud.server_self_update.reload_systemd")(function* () {
@@ -26603,7 +26694,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (flags, run
26603
26694
  });
26604
26695
  yield* Console.log(output);
26605
26696
  }));
26606
- const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)));
26697
+ const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(Layer.provide(layer$67(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)));
26607
26698
  return yield* Effect.gen(function* () {
26608
26699
  const snapshot = yield* getOfflineSnapshot();
26609
26700
  const orchestrationEngine = yield* OrchestrationEngineService;
@@ -26614,7 +26705,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (flags, run
26614
26705
  });
26615
26706
  yield* Console.log(output);
26616
26707
  }).pipe(Effect.provide(offlineRuntimeLayer));
26617
- }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer, layer$42).pipe(Layer.provideMerge(FetchHttpClient.layer), Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
26708
+ }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer, layer$42).pipe(Layer.provideMerge(FetchHttpClient.layer), Layer.provide(layer$67(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
26618
26709
  });
26619
26710
  const projectAddCommand = Command.make("add", {
26620
26711
  ...projectLocationFlags,
@@ -43610,7 +43701,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
43610
43701
  };
43611
43702
  });
43612
43703
  var OpenCodeRuntime = class extends Context.Service()("@p4code/cli/provider/opencodeRuntime") {};
43613
- const OpenCodeRuntimeLive = Layer.effect(OpenCodeRuntime, makeOpenCodeRuntime).pipe(Layer.provide(layer$67));
43704
+ const OpenCodeRuntimeLive = Layer.effect(OpenCodeRuntime, makeOpenCodeRuntime).pipe(Layer.provide(layer$68));
43614
43705
  //#endregion
43615
43706
  //#region src/textGeneration/TextGenerationUtils.ts
43616
43707
  const isTextGenerationError$2 = Schema$1.is(TextGenerationError);
@@ -82703,9 +82794,9 @@ const WorkspaceEntriesLayerLive = layer$26.pipe(Layer.provide(layer$42));
82703
82794
  const WorkspaceFileSystemLayerLive = layer$25.pipe(Layer.provide(layer$42), Layer.provide(WorkspaceEntriesLayerLive));
82704
82795
  const WorkspaceLayerLive = Layer.mergeAll(layer$42, WorkspaceEntriesLayerLive, WorkspaceFileSystemLayerLive);
82705
82796
  const ProjectFaviconResolverLayerLive = layer$40.pipe(Layer.provide(layer$42), Layer.provide(layer$41));
82706
- const AuthLayerLive = layer$59.pipe(Layer.provideMerge(PersistenceLayerLive), Layer.provide(layer$63));
82797
+ const AuthLayerLive = layer$60.pipe(Layer.provideMerge(PersistenceLayerLive), Layer.provide(layer$64));
82707
82798
  const ProviderRuntimeLayerLive = ProviderSessionReaperLive.pipe(Layer.provideMerge(ProviderLayerLive), Layer.provideMerge(OrchestrationLayerLive));
82708
- const RuntimeDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(CheckpointingLayerLive), Layer.provideMerge(SourceControlProviderRegistryLayerLive), Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)), Layer.provideMerge(PersistenceLayerLive), Layer.provideMerge(layer$50), Layer.provideMerge(ProviderRegistryLive), Layer.provideMerge(ProviderInstanceRegistryHydrationLive), Layer.provideMerge(ProviderEventLoggersLive), Layer.provideMerge(OpenCodeRuntimeLive), Layer.provideMerge(layer$57.pipe(Layer.provide(layer$63))), Layer.provideMerge(WorkspaceLayerLive), Layer.provideMerge(ProjectFaviconResolverLayerLive), Layer.provideMerge(layer$51), Layer.provideMerge(layer$44), Layer.provideMerge(AuthLayerLive), Layer.provideMerge(layer$56), Layer.provideMerge(layer$63)).pipe(Layer.provideMerge(layer$8), Layer.provideMerge(layer$7), Layer.provideMerge(layer$6), Layer.provideMerge(layer$47), Layer.provideMerge(layer$49), Layer.provideMerge(layer$48), Layer.provide(layer$67));
82799
+ const RuntimeDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(CheckpointingLayerLive), Layer.provideMerge(SourceControlProviderRegistryLayerLive), Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)), Layer.provideMerge(PersistenceLayerLive), Layer.provideMerge(layer$50), Layer.provideMerge(ProviderRegistryLive), Layer.provideMerge(ProviderInstanceRegistryHydrationLive), Layer.provideMerge(ProviderEventLoggersLive), Layer.provideMerge(OpenCodeRuntimeLive), Layer.provideMerge(layer$57.pipe(Layer.provide(layer$64))), Layer.provideMerge(WorkspaceLayerLive), Layer.provideMerge(ProjectFaviconResolverLayerLive), Layer.provideMerge(layer$51), Layer.provideMerge(layer$44), Layer.provideMerge(AuthLayerLive), Layer.provideMerge(layer$56), Layer.provideMerge(layer$64)).pipe(Layer.provideMerge(layer$8), Layer.provideMerge(layer$7), Layer.provideMerge(layer$6), Layer.provideMerge(layer$47), Layer.provideMerge(layer$49), Layer.provideMerge(layer$48), Layer.provide(layer$68));
82709
82800
  /**
82710
82801
  * Hub asset sync.
82711
82802
  *
@@ -82773,6 +82864,151 @@ const serveCommand = Command.make("serve", { ...sharedServerCommandFlags }).pipe
82773
82864
  forceAutoBootstrapProjectFromCwd: false
82774
82865
  })));
82775
82866
  //#endregion
82867
+ //#region src/service/cliSelfUpdate.ts
82868
+ /**
82869
+ * Upgrading the globally installed CLI, then the service that runs it.
82870
+ *
82871
+ * Two steps that have to happen in that order and from different binaries, and
82872
+ * getting the order wrong is silent: `service update` renders the unit from the
82873
+ * *running* CLI, and pins that version into the runtime directory. Run the
82874
+ * reconcile from the old process and you install a new CLI and then carefully
82875
+ * point the service back at the old one.
82876
+ *
82877
+ * So the second step re-executes the newly installed binary. That is also why
82878
+ * this is not `ServerSelfUpdate`: that path respawns the server in place, which
82879
+ * it explicitly refuses to do under launchd — KeepAlive would restart the
82880
+ * process being replaced and leave two servers on one port. Under a supervisor
82881
+ * the honest sequence is install, then let the supervisor restart, which is
82882
+ * exactly what `service update` does.
82883
+ *
82884
+ * @module service/cliSelfUpdate
82885
+ */
82886
+ /** npm can take minutes when a native dependency compiles from source. */
82887
+ const CLI_INSTALL_TIMEOUT_MS = 600 * 1e3;
82888
+ const PACKAGE_NAME = "@p4code/cli";
82889
+ /**
82890
+ * Exact versions or the two dist-tags, and nothing else.
82891
+ *
82892
+ * The value is handed to npm and printed back as "installed X". An arbitrary
82893
+ * string could be a git URL or a local path, which npm would happily install
82894
+ * and which would make the acknowledgement a lie.
82895
+ */
82896
+ const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?$/u;
82897
+ const isInstallableVersion = (value) => value === "latest" || value === "next" || EXACT_VERSION_PATTERN.test(value);
82898
+ var CliSelfUpdateError = class extends Schema$1.TaggedErrorClass()("CliSelfUpdateError", {
82899
+ step: Schema$1.String,
82900
+ detail: Schema$1.optional(Schema$1.String)
82901
+ }) {
82902
+ get message() {
82903
+ return this.detail === void 0 ? `Self-update failed while ${this.step}.` : `Self-update failed while ${this.step}: ${this.detail}`;
82904
+ }
82905
+ };
82906
+ /**
82907
+ * Where `npm install --global` put the executable.
82908
+ *
82909
+ * Asked of npm rather than assumed, because the answer differs between a
82910
+ * Homebrew node, an nvm one and a system one — and the whole point of this step
82911
+ * is to run the binary that was just written, not the one on this process's
82912
+ * PATH, which a shell may have cached.
82913
+ */
82914
+ const resolveGlobalBinary = Effect.fn("cli.selfUpdate.resolveGlobalBinary")(function* (binaryName) {
82915
+ const result = yield* (yield* ProcessRunner).run({
82916
+ command: "npm",
82917
+ args: ["prefix", "--global"]
82918
+ }).pipe(Effect.mapError((cause) => new CliSelfUpdateError({
82919
+ step: "locating the npm global prefix",
82920
+ detail: String(cause)
82921
+ })));
82922
+ if (result.code !== 0) return yield* new CliSelfUpdateError({
82923
+ step: "locating the npm global prefix",
82924
+ detail: `npm exited with code ${String(result.code)}`
82925
+ });
82926
+ const prefix = result.stdout.trim();
82927
+ if (prefix.length === 0) return yield* new CliSelfUpdateError({
82928
+ step: "locating the npm global prefix",
82929
+ detail: "npm printed no prefix"
82930
+ });
82931
+ return `${prefix}/bin/${binaryName}`;
82932
+ });
82933
+ const installGlobalCli = Effect.fn("cli.selfUpdate.installGlobalCli")(function* (input) {
82934
+ const runner = yield* ProcessRunner;
82935
+ const step = `installing ${PACKAGE_NAME}@${input.version}`;
82936
+ const result = yield* runner.run({
82937
+ command: "npm",
82938
+ args: [
82939
+ "install",
82940
+ "--global",
82941
+ "--no-fund",
82942
+ "--no-audit",
82943
+ `${PACKAGE_NAME}@${input.version}`
82944
+ ],
82945
+ timeout: CLI_INSTALL_TIMEOUT_MS
82946
+ }).pipe(Effect.mapError((cause) => new CliSelfUpdateError({
82947
+ step,
82948
+ detail: String(cause)
82949
+ })));
82950
+ if (result.code !== 0) return yield* new CliSelfUpdateError({
82951
+ step,
82952
+ detail: result.stderr.trim().length > 0 ? result.stderr.trim() : `exit code ${String(result.code)}`
82953
+ });
82954
+ });
82955
+ /**
82956
+ * Ask the freshly installed binary what version it is.
82957
+ *
82958
+ * A completed npm install can still be unusable — a native dependency that
82959
+ * failed to build, a Node too old — and it is better to find that out here,
82960
+ * while the service is still running the version that works, than after
82961
+ * pointing the unit at it.
82962
+ */
82963
+ const readInstalledVersion = Effect.fn("cli.selfUpdate.readInstalledVersion")(function* (binaryPath) {
82964
+ const runner = yield* ProcessRunner;
82965
+ const step = "verifying the installed CLI";
82966
+ const result = yield* runner.run({
82967
+ command: binaryPath,
82968
+ args: ["--version"]
82969
+ }).pipe(Effect.mapError((cause) => new CliSelfUpdateError({
82970
+ step,
82971
+ detail: String(cause)
82972
+ })));
82973
+ if (result.code !== 0) return yield* new CliSelfUpdateError({
82974
+ step,
82975
+ detail: `the installed CLI exited with code ${String(result.code)}`
82976
+ });
82977
+ const match = /(\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?)/u.exec(result.stdout);
82978
+ if (match?.[1] === void 0) return yield* new CliSelfUpdateError({
82979
+ step,
82980
+ detail: "the installed CLI did not report a version"
82981
+ });
82982
+ return match[1];
82983
+ });
82984
+ /**
82985
+ * Hand the rest of the work to the new binary.
82986
+ *
82987
+ * `service update` from *this* process would pin this process's version. The
82988
+ * new binary reconciles to itself, which is the only way the unit ends up
82989
+ * pointing at what was just installed.
82990
+ */
82991
+ const reconcileWithNewBinary = Effect.fn("cli.selfUpdate.reconcileWithNewBinary")(function* (input) {
82992
+ const runner = yield* ProcessRunner;
82993
+ const step = "updating the service to the new version";
82994
+ const result = yield* runner.run({
82995
+ command: input.binaryPath,
82996
+ args: [
82997
+ "service",
82998
+ "update",
82999
+ ...input.baseDir === void 0 ? [] : ["--base-dir", input.baseDir]
83000
+ ],
83001
+ timeout: CLI_INSTALL_TIMEOUT_MS
83002
+ }).pipe(Effect.mapError((cause) => new CliSelfUpdateError({
83003
+ step,
83004
+ detail: String(cause)
83005
+ })));
83006
+ if (result.code !== 0) return yield* new CliSelfUpdateError({
83007
+ step,
83008
+ detail: result.stderr.trim().length > 0 ? result.stderr.trim() : `exit code ${String(result.code)}`
83009
+ });
83010
+ });
83011
+ //#endregion
82776
83012
  //#region src/cli/service.ts
82777
83013
  const isBootServiceCommandError = Schema$1.is(BootServiceCommandError);
82778
83014
  const bootServiceLayer = (config) => layer$46({
@@ -82918,6 +83154,39 @@ const serviceRestartCommand = Command.make("restart", projectLocationFlags).pipe
82918
83154
  yield* service.restart;
82919
83155
  yield* Console.log("Restarted the P4Code service.");
82920
83156
  }))));
83157
+ /**
83158
+ * Install a newer CLI, then point the service at it.
83159
+ *
83160
+ * Deliberately not `ServerSelfUpdate`, which the server uses to replace itself
83161
+ * in place: that path refuses to run under launchd, because KeepAlive would
83162
+ * restart the process being replaced and leave two servers on one port. Under a
83163
+ * supervisor the honest sequence is install, then reconcile and let the
83164
+ * supervisor restart — which is what this does, from the new binary.
83165
+ */
83166
+ const serviceSelfUpdateCommand = Command.make("self-update", {
83167
+ ...projectLocationFlags,
83168
+ version: Flag.string("version").pipe(Flag.withDescription("Version or dist-tag to install (default latest)."), Flag.optional)
83169
+ }).pipe(Command.withDescription("Install the newest @p4code/cli and update the service to run it."), Command.withHandler((flags) => runServiceCommand(flags, Effect.gen(function* () {
83170
+ const requested = Option.getOrElse(flags.version, () => "latest");
83171
+ if (!isInstallableVersion(requested)) {
83172
+ yield* Console.error(`'${requested}' is not a version or dist-tag. Use an exact version, or latest.`);
83173
+ return;
83174
+ }
83175
+ yield* Console.log(`Installing ${PACKAGE_NAME}@${requested}…`);
83176
+ yield* installGlobalCli({ version: requested });
83177
+ const binaryPath = yield* resolveGlobalBinary("p4c");
83178
+ const installed = yield* readInstalledVersion(binaryPath);
83179
+ if (installed === version) {
83180
+ yield* Console.log(`Already running @p4code/cli@${installed}; nothing to update.`);
83181
+ return;
83182
+ }
83183
+ yield* Console.log(`Installed @p4code/cli@${installed}; updating the service…`);
83184
+ yield* reconcileWithNewBinary({
83185
+ binaryPath,
83186
+ baseDir: void 0
83187
+ });
83188
+ yield* Console.log(`Service now runs @p4code/cli@${installed}.`);
83189
+ }))));
82921
83190
  const serviceStatusCommand = Command.make("status", projectLocationFlags).pipe(Command.withDescription("Show whether the P4Code background service is installed."), Command.withHandler((flags) => runServiceCommand(flags, Effect.gen(function* () {
82922
83191
  const service = yield* BootService;
82923
83192
  yield* Console.log(formatServiceStatus(yield* service.status, version));
@@ -82942,11 +83211,12 @@ const serviceCommand = Command.make("service").pipe(Command.withDescription("Man
82942
83211
  serviceUninstallCommand,
82943
83212
  serviceUpdateCommand,
82944
83213
  serviceRestartCommand,
83214
+ serviceSelfUpdateCommand,
82945
83215
  serviceStatusCommand
82946
83216
  ]));
82947
83217
  //#endregion
82948
83218
  //#region src/bin.ts
82949
- const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, layer$67, FetchHttpClient.layer);
83219
+ const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, layer$68, FetchHttpClient.layer);
82950
83220
  /**
82951
83221
  * Prints an update reminder before dispatching, at most once a day.
82952
83222
  *