@p4code/cli 0.0.36 → 0.0.38

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
@@ -54,6 +54,9 @@ import * as Exit from "effect/Exit";
54
54
  import * as SchemaGetter from "effect/SchemaGetter";
55
55
  import * as Config from "effect/Config";
56
56
  import * as NodeReadline from "node:readline";
57
+ import * as Scope from "effect/Scope";
58
+ import * as ChildProcess$1 from "effect/unstable/process/ChildProcess";
59
+ import * as ChildProcessSpawner$1 from "effect/unstable/process/ChildProcessSpawner";
57
60
  import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
58
61
  import * as Clock from "effect/Clock";
59
62
  import * as Deferred from "effect/Deferred";
@@ -63,9 +66,6 @@ import { cast, dual } from "effect/Function";
63
66
  import Mime from "@effect/platform-node/Mime";
64
67
  import * as Arr from "effect/Array";
65
68
  import * as Cache from "effect/Cache";
66
- import * as Scope from "effect/Scope";
67
- import * as ChildProcess$1 from "effect/unstable/process/ChildProcess";
68
- import * as ChildProcessSpawner$1 from "effect/unstable/process/ChildProcessSpawner";
69
69
  import * as Semaphore from "effect/Semaphore";
70
70
  import * as Equal from "effect/Equal";
71
71
  import * as HttpClient$1 from "effect/unstable/http/HttpClient";
@@ -232,7 +232,7 @@ const make$67 = () => {
232
232
  const layer$62 = Layer.sync(NetService, make$67);
233
233
  //#endregion
234
234
  //#region package.json
235
- var version = "0.0.36";
235
+ var version = "0.0.38";
236
236
  //#endregion
237
237
  //#region src/config.ts
238
238
  /**
@@ -916,6 +916,11 @@ const resolveLatestVersion = Effect.fn("cli.updateCheck.resolve")(function* (sta
916
916
  }).pipe(Effect.flatMap((encoded) => fs.writeFileString(cachePath, encoded)), Effect.ignore);
917
917
  return latestVersion;
918
918
  });
919
+ /**
920
+ * Both commands, because `npm i -g` only replaces the binary on disk. The
921
+ * service is a running process still executing the old code until
922
+ * `service update` restarts it onto the new one.
923
+ */
919
924
  function formatUpdateReminder(input) {
920
925
  return [`Update available: ${PACKAGE_NAME}@${input.latestVersion} (running ${input.currentVersion}).`, ` npm i -g ${PACKAGE_NAME}@latest && p4 service update`].join("\n");
921
926
  }
@@ -10835,7 +10840,7 @@ Layer.effectDiscard(runMigrations());
10835
10840
  //#region src/persistence/Layers/Sqlite.ts
10836
10841
  const defaultSqliteClientLoaders = {
10837
10842
  bun: () => import("@effect/sql-sqlite-bun/SqliteClient"),
10838
- node: () => import("./NodeSqliteClient-BUlNQAY0.mjs")
10843
+ node: () => import("./NodeSqliteClient-Dq296EBh.mjs")
10839
10844
  };
10840
10845
  const makeRuntimeSqliteLayer = Effect.fn("makeRuntimeSqliteLayer")(function* (config) {
10841
10846
  const runtime = process.versions.bun !== void 0 ? "bun" : "node";
@@ -12030,6 +12035,420 @@ const sessionCommand = Command.make("session").pipe(Command.withDescription("Man
12030
12035
  ]));
12031
12036
  const authCommand = Command.make("auth").pipe(Command.withDescription("Manage the local auth control plane for headless deployments."), Command.withSubcommands([pairingCommand, sessionCommand]));
12032
12037
  //#endregion
12038
+ //#region src/stream/collectUint8StreamText.ts
12039
+ const collectUint8StreamText = (input) => {
12040
+ const maxBytes = input.maxBytes ?? Number.POSITIVE_INFINITY;
12041
+ const truncatedMarker = input.truncatedMarker ?? "";
12042
+ return input.stream.pipe(Stream.runFold(() => ({
12043
+ chunks: [],
12044
+ bytes: 0,
12045
+ truncated: false
12046
+ }), (state, chunk) => {
12047
+ if (state.truncated) return state;
12048
+ const remainingBytes = maxBytes - state.bytes;
12049
+ if (remainingBytes <= 0) return {
12050
+ ...state,
12051
+ truncated: true
12052
+ };
12053
+ const nextChunk = chunk.byteLength > remainingBytes ? chunk.slice(0, remainingBytes) : chunk;
12054
+ state.chunks.push(nextChunk);
12055
+ const bytes = state.bytes + nextChunk.byteLength;
12056
+ const truncated = chunk.byteLength > remainingBytes;
12057
+ return {
12058
+ chunks: state.chunks,
12059
+ bytes,
12060
+ truncated
12061
+ };
12062
+ }), Effect.map((state) => {
12063
+ const text = Buffer.concat(state.chunks, state.bytes).toString("utf8");
12064
+ return {
12065
+ text: state.truncated && truncatedMarker.length > 0 ? `${text}${truncatedMarker}` : text,
12066
+ bytes: state.bytes,
12067
+ truncated: state.truncated
12068
+ };
12069
+ }));
12070
+ };
12071
+ //#endregion
12072
+ //#region src/processRunner.ts
12073
+ const ProcessInvocationFields = {
12074
+ command: Schema$1.String,
12075
+ argumentCount: Schema$1.Number,
12076
+ cwd: Schema$1.optional(Schema$1.String),
12077
+ spawnCwd: Schema$1.optional(Schema$1.String)
12078
+ };
12079
+ const formatProcessInvocation = (input) => {
12080
+ const executionCwd = input.spawnCwd ?? input.cwd;
12081
+ return executionCwd === void 0 ? `'${input.command}'` : `'${input.command}' in '${executionCwd}'`;
12082
+ };
12083
+ var ProcessSpawnError = class extends Schema$1.TaggedErrorClass()("ProcessSpawnError", {
12084
+ ...ProcessInvocationFields,
12085
+ resolvedCommand: Schema$1.optional(Schema$1.String),
12086
+ resolvedArgumentCount: Schema$1.optional(Schema$1.Number),
12087
+ shell: Schema$1.optional(Schema$1.Boolean),
12088
+ cause: Schema$1.Defect()
12089
+ }) {
12090
+ get message() {
12091
+ return `Failed to spawn process ${formatProcessInvocation(this)}`;
12092
+ }
12093
+ };
12094
+ var ProcessStdinError = class extends Schema$1.TaggedErrorClass()("ProcessStdinError", {
12095
+ ...ProcessInvocationFields,
12096
+ stdinBytes: Schema$1.Number,
12097
+ cause: Schema$1.Defect()
12098
+ }) {
12099
+ get message() {
12100
+ return `Failed to write stdin for process ${formatProcessInvocation(this)}`;
12101
+ }
12102
+ };
12103
+ var ProcessOutputLimitError = class extends Schema$1.TaggedErrorClass()("ProcessOutputLimitError", {
12104
+ ...ProcessInvocationFields,
12105
+ stream: Schema$1.Literals(["stdout", "stderr"]),
12106
+ maxBytes: Schema$1.Number,
12107
+ observedBytes: Schema$1.Number
12108
+ }) {
12109
+ get message() {
12110
+ return `Process ${formatProcessInvocation(this)} ${this.stream} produced ${this.observedBytes} bytes, exceeding the ${this.maxBytes} byte limit`;
12111
+ }
12112
+ };
12113
+ var ProcessReadError = class extends Schema$1.TaggedErrorClass()("ProcessReadError", {
12114
+ ...ProcessInvocationFields,
12115
+ stream: Schema$1.Literals([
12116
+ "stdout",
12117
+ "stderr",
12118
+ "exitCode"
12119
+ ]),
12120
+ cause: Schema$1.Defect()
12121
+ }) {
12122
+ get message() {
12123
+ return `Failed to read ${this.stream} for process ${formatProcessInvocation(this)}`;
12124
+ }
12125
+ };
12126
+ var ProcessTimeoutError = class extends Schema$1.TaggedErrorClass()("ProcessTimeoutError", {
12127
+ ...ProcessInvocationFields,
12128
+ timeoutMs: Schema$1.Number
12129
+ }) {
12130
+ get message() {
12131
+ return `Process ${formatProcessInvocation(this)} timed out after ${this.timeoutMs}ms`;
12132
+ }
12133
+ };
12134
+ Schema$1.Union([
12135
+ ProcessSpawnError,
12136
+ ProcessStdinError,
12137
+ ProcessOutputLimitError,
12138
+ ProcessReadError,
12139
+ ProcessTimeoutError
12140
+ ]);
12141
+ var ProcessRunner = class extends Context.Service()("@p4code/cli/processRunner") {};
12142
+ const DEFAULT_TIMEOUT = "60 seconds";
12143
+ const DEFAULT_MAX_OUTPUT_BYTES$2 = 8 * 1024 * 1024;
12144
+ const WINDOWS_COMMAND_NOT_FOUND_PATTERNS = [
12145
+ /is not recognized as an internal or external command/i,
12146
+ /n.o . reconhecido como um comando interno/i,
12147
+ /non . riconosciuto come comando interno o esterno/i,
12148
+ /n.est pas reconnu en tant que commande interne/i,
12149
+ /no se reconoce como un comando interno o externo/i,
12150
+ /wird nicht als interner oder externer befehl/i
12151
+ ];
12152
+ function hasWindowsCommandNotFoundMessage(output) {
12153
+ return WINDOWS_COMMAND_NOT_FOUND_PATTERNS.some((pattern) => pattern.test(output));
12154
+ }
12155
+ const isWindowsCommandNotFound = Effect.fn("processRunner.isWindowsCommandNotFound")(function* (code, stderr) {
12156
+ if ((yield* HostProcessPlatform) !== "win32") return false;
12157
+ if (code === 9009) return true;
12158
+ return hasWindowsCommandNotFoundMessage(stderr);
12159
+ });
12160
+ const collectText = Effect.fn("processRunner.collectText")(function* (input) {
12161
+ const stream = input.stream.pipe(Stream.mapError((cause) => new ProcessReadError({
12162
+ command: input.command,
12163
+ argumentCount: input.args.length,
12164
+ cwd: input.cwd,
12165
+ spawnCwd: input.spawnCwd,
12166
+ stream: input.streamName,
12167
+ cause
12168
+ })));
12169
+ if (input.outputMode === "truncate") return yield* collectUint8StreamText({
12170
+ stream,
12171
+ maxBytes: input.maxOutputBytes,
12172
+ truncatedMarker: input.truncatedMarker
12173
+ });
12174
+ return yield* stream.pipe(Stream.runFoldEffect(() => ({
12175
+ chunks: [],
12176
+ bytes: 0
12177
+ }), (state, chunk) => {
12178
+ const remainingBytes = input.maxOutputBytes - state.bytes;
12179
+ if (chunk.byteLength > remainingBytes) return Effect.fail(new ProcessOutputLimitError({
12180
+ command: input.command,
12181
+ argumentCount: input.args.length,
12182
+ cwd: input.cwd,
12183
+ spawnCwd: input.spawnCwd,
12184
+ stream: input.streamName,
12185
+ maxBytes: input.maxOutputBytes,
12186
+ observedBytes: state.bytes + chunk.byteLength
12187
+ }));
12188
+ state.chunks.push(chunk);
12189
+ return Effect.succeed({
12190
+ chunks: state.chunks,
12191
+ bytes: state.bytes + chunk.byteLength
12192
+ });
12193
+ }), Effect.map((state) => ({
12194
+ text: Buffer.concat(state.chunks, state.bytes).toString("utf8"),
12195
+ bytes: state.bytes,
12196
+ truncated: false
12197
+ })));
12198
+ });
12199
+ function finalizeRunProcess(effect, input) {
12200
+ const timeout = Duration.fromInputUnsafe(input.timeout ?? DEFAULT_TIMEOUT);
12201
+ const timeoutBehavior = input.timeoutBehavior ?? "error";
12202
+ return effect.pipe(Effect.scoped, Effect.timeoutOption(timeout), Effect.flatMap((result) => {
12203
+ if (Option.isSome(result)) return Effect.succeed(result.value);
12204
+ if (timeoutBehavior === "timedOutResult") return Effect.succeed({
12205
+ stdout: "",
12206
+ stderr: "",
12207
+ code: null,
12208
+ timedOut: true,
12209
+ stdoutTruncated: false,
12210
+ stderrTruncated: false
12211
+ });
12212
+ return Effect.fail(new ProcessTimeoutError({
12213
+ command: input.command,
12214
+ argumentCount: input.args.length,
12215
+ cwd: input.cwd,
12216
+ spawnCwd: input.spawnCwd,
12217
+ timeoutMs: Duration.toMillis(timeout)
12218
+ }));
12219
+ }));
12220
+ }
12221
+ const runProcessCore = Effect.fn("processRunner.runProcessCore")(function* (spawner, input) {
12222
+ const maxOutputBytes = input.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES$2;
12223
+ const outputMode = input.outputMode ?? "error";
12224
+ const truncatedMarker = input.truncatedMarker ?? "";
12225
+ const extendEnv = input.env !== void 0;
12226
+ const spawnCommand = yield* resolveSpawnCommand(input.command, input.args, input.env === void 0 ? {} : {
12227
+ env: input.env,
12228
+ extendEnv
12229
+ });
12230
+ const child = yield* spawner.spawn(ChildProcess$1.make(spawnCommand.command, spawnCommand.args, {
12231
+ ...input.spawnCwd ?? input.cwd ? { cwd: input.spawnCwd ?? input.cwd } : {},
12232
+ ...input.env !== void 0 ? {
12233
+ env: input.env,
12234
+ extendEnv
12235
+ } : {},
12236
+ shell: spawnCommand.shell
12237
+ })).pipe(Effect.mapError((cause) => new ProcessSpawnError({
12238
+ command: input.command,
12239
+ argumentCount: input.args.length,
12240
+ cwd: input.cwd,
12241
+ spawnCwd: input.spawnCwd,
12242
+ resolvedCommand: spawnCommand.command,
12243
+ resolvedArgumentCount: spawnCommand.args.length,
12244
+ shell: spawnCommand.shell,
12245
+ cause
12246
+ })));
12247
+ const stdin = input.stdin;
12248
+ const writeStdin = stdin === void 0 ? Effect.void : Stream.run(Stream.encodeText(Stream.make(stdin)), child.stdin).pipe(Effect.mapError((cause) => new ProcessStdinError({
12249
+ command: input.command,
12250
+ argumentCount: input.args.length,
12251
+ cwd: input.cwd,
12252
+ spawnCwd: input.spawnCwd,
12253
+ stdinBytes: Buffer.byteLength(stdin),
12254
+ cause
12255
+ })));
12256
+ const [stdout, stderr] = yield* Effect.all([
12257
+ collectText({
12258
+ command: input.command,
12259
+ args: input.args,
12260
+ cwd: input.cwd,
12261
+ spawnCwd: input.spawnCwd,
12262
+ streamName: "stdout",
12263
+ stream: child.stdout,
12264
+ maxOutputBytes,
12265
+ outputMode,
12266
+ truncatedMarker
12267
+ }),
12268
+ collectText({
12269
+ command: input.command,
12270
+ args: input.args,
12271
+ cwd: input.cwd,
12272
+ spawnCwd: input.spawnCwd,
12273
+ streamName: "stderr",
12274
+ stream: child.stderr,
12275
+ maxOutputBytes,
12276
+ outputMode,
12277
+ truncatedMarker
12278
+ }),
12279
+ writeStdin
12280
+ ], { concurrency: "unbounded" });
12281
+ const exitCode = yield* child.exitCode.pipe(Effect.mapError((cause) => new ProcessReadError({
12282
+ command: input.command,
12283
+ argumentCount: input.args.length,
12284
+ cwd: input.cwd,
12285
+ spawnCwd: input.spawnCwd,
12286
+ stream: "exitCode",
12287
+ cause
12288
+ })));
12289
+ return {
12290
+ stdout: stdout.text,
12291
+ stderr: stderr.text,
12292
+ code: exitCode,
12293
+ timedOut: false,
12294
+ stdoutTruncated: stdout.truncated,
12295
+ stderrTruncated: stderr.truncated
12296
+ };
12297
+ });
12298
+ const make$58 = Effect.fn("ProcessRunner.make")(function* () {
12299
+ const spawner = yield* ChildProcessSpawner$1.ChildProcessSpawner;
12300
+ const run = (input) => finalizeRunProcess(runProcessCore(spawner, input), input);
12301
+ return ProcessRunner.of({ run });
12302
+ });
12303
+ const layer$53 = Layer.effect(ProcessRunner, make$58());
12304
+ //#endregion
12305
+ //#region src/cli/completions.ts
12306
+ /**
12307
+ * Installs the zsh completion script for the `p4` command.
12308
+ *
12309
+ * zsh ships `_perforce` with `#compdef p4 p4d`, so out of the box tabbing
12310
+ * through our subcommands prints "unhandled perforce command: …" from its
12311
+ * fallback message. `compinit` resolves duplicate `#compdef` claims by fpath
12312
+ * order — first match wins — and `_perforce` lives in the *last* fpath entry
12313
+ * (`/usr/share/zsh/<ver>/functions`). Writing our own `_p4` into any earlier
12314
+ * directory therefore replaces it outright, with no shell-config edit.
12315
+ */
12316
+ const COMPLETION_FILE = "_p4";
12317
+ const GENERATE_TIMEOUT_MS = 1e4;
12318
+ /**
12319
+ * Directories ahead of `/usr/share/zsh/*` in a default fpath, most specific
12320
+ * first. `~/.zfunc` is the fallback and is the only one needing an fpath line.
12321
+ */
12322
+ function candidateDirectories(input) {
12323
+ return [
12324
+ {
12325
+ dir: input.join(input.homeDir, ".oh-my-zsh", "custom", "completions"),
12326
+ requiresFpathEntry: false
12327
+ },
12328
+ {
12329
+ dir: "/opt/homebrew/share/zsh/site-functions",
12330
+ requiresFpathEntry: false
12331
+ },
12332
+ {
12333
+ dir: "/usr/local/share/zsh/site-functions",
12334
+ requiresFpathEntry: false
12335
+ },
12336
+ {
12337
+ dir: input.join(input.homeDir, ".zfunc"),
12338
+ requiresFpathEntry: true
12339
+ }
12340
+ ];
12341
+ }
12342
+ /**
12343
+ * Asks the running CLI for its own completion script rather than rendering one
12344
+ * here, so the output cannot drift from what `--completions` produces.
12345
+ */
12346
+ const generateZshCompletionScript = Effect.fn("cli.completions.generate")(function* () {
12347
+ const runner = yield* ProcessRunner;
12348
+ const execPath = yield* HostProcessExecutablePath;
12349
+ const entryPath = (yield* HostProcessArguments)[1] ?? "";
12350
+ if (entryPath === "") return null;
12351
+ const result = yield* runner.run({
12352
+ command: execPath,
12353
+ args: [
12354
+ entryPath,
12355
+ "--completions",
12356
+ "zsh"
12357
+ ],
12358
+ timeout: GENERATE_TIMEOUT_MS
12359
+ });
12360
+ if (result.code !== 0) return null;
12361
+ const script = result.stdout.trim();
12362
+ return script.length === 0 ? null : `${script}\n`;
12363
+ });
12364
+ /**
12365
+ * Best-effort: returns null when completions are not applicable (Windows, no
12366
+ * writable directory) rather than failing the command that asked for them.
12367
+ * Installing completions is never the reason an install should fail.
12368
+ */
12369
+ const installZshCompletion = Effect.fn("cli.completions.install")(function* () {
12370
+ if ((yield* HostProcessPlatform) === "win32") return null;
12371
+ const fs = yield* FileSystem.FileSystem;
12372
+ const path = yield* Path.Path;
12373
+ const homeDir = (yield* HostProcessEnvironment).HOME ?? "";
12374
+ if (homeDir === "") return null;
12375
+ const script = yield* generateZshCompletionScript();
12376
+ if (script === null) return null;
12377
+ for (const candidate of candidateDirectories({
12378
+ homeDir,
12379
+ join: path.join
12380
+ })) {
12381
+ if (candidate.requiresFpathEntry) {
12382
+ if (!(yield* fs.makeDirectory(candidate.dir, { recursive: true }).pipe(Effect.as(true), Effect.orElseSucceed(() => false)))) continue;
12383
+ } else if (!(yield* fs.exists(candidate.dir).pipe(Effect.orElseSucceed(() => false)))) continue;
12384
+ const target = path.join(candidate.dir, COMPLETION_FILE);
12385
+ if (!(yield* fs.writeFileString(target, script).pipe(Effect.as(true), Effect.orElseSucceed(() => false)))) continue;
12386
+ return {
12387
+ path: target,
12388
+ requiresFpathEntry: candidate.requiresFpathEntry
12389
+ };
12390
+ }
12391
+ return null;
12392
+ });
12393
+ const HINT_MARKER_FILE = "completions-hint";
12394
+ const HINT_INTERVAL_MS = 1440 * 60 * 1e3;
12395
+ const COMPLETIONS_HINT = "Shell completions are not installed, so tab completion shows Perforce's. Run `p4 completions install`.";
12396
+ /**
12397
+ * A hint when no `_p4` is on the fpath, at most once a day.
12398
+ *
12399
+ * A stat of a few directories, deliberately not the install itself: generating
12400
+ * the script spawns a subprocess, which is far too much to do on every command
12401
+ * just in case. Throttled through a marker file so an unactioned hint does not
12402
+ * become noise on every invocation.
12403
+ */
12404
+ const resolveMissingCompletionsHint = Effect.fn("cli.completions.hint")(function* (input) {
12405
+ if ((yield* HostProcessPlatform) === "win32") return null;
12406
+ const fs = yield* FileSystem.FileSystem;
12407
+ const path = yield* Path.Path;
12408
+ const homeDir = (yield* HostProcessEnvironment).HOME ?? "";
12409
+ if (homeDir === "") return null;
12410
+ for (const candidate of candidateDirectories({
12411
+ homeDir,
12412
+ join: path.join
12413
+ })) if (yield* fs.exists(path.join(candidate.dir, COMPLETION_FILE)).pipe(Effect.orElseSucceed(() => false))) return null;
12414
+ const markerPath = path.join(input.stateDir, HINT_MARKER_FILE);
12415
+ const marker = yield* fs.stat(markerPath).pipe(Effect.asSome, Effect.orElseSucceed(() => null));
12416
+ const nowMs = DateTime.toEpochMillis(yield* DateTime.now);
12417
+ if (marker !== null && Option.isSome(marker)) {
12418
+ const mtime = marker.value.mtime;
12419
+ if (Option.isSome(mtime) && nowMs - mtime.value.getTime() < HINT_INTERVAL_MS) return null;
12420
+ }
12421
+ yield* fs.makeDirectory(input.stateDir, { recursive: true }).pipe(Effect.ignore);
12422
+ yield* fs.writeFileString(markerPath, "").pipe(Effect.ignore);
12423
+ return COMPLETIONS_HINT;
12424
+ });
12425
+ /**
12426
+ * Standalone so completions never depend on the service. A laptop that only
12427
+ * runs clients has no service to install, and a host whose `service install`
12428
+ * fails still deserves working completions.
12429
+ */
12430
+ const completionsCommand = Command.make("completions").pipe(Command.withDescription("Manage shell completions for the p4 command."), Command.withSubcommands([Command.make("install").pipe(Command.withDescription("Install zsh completions, replacing the Perforce completion that claims `p4`."), Command.withHandler(() => Effect.gen(function* () {
12431
+ const install = yield* installZshCompletion();
12432
+ if (install === null) {
12433
+ yield* Console.error([
12434
+ "Could not install completions automatically.",
12435
+ "Write them yourself with:",
12436
+ " p4 --completions zsh > <a directory on your fpath>/_p4"
12437
+ ].join("\n"));
12438
+ return;
12439
+ }
12440
+ yield* Console.log(formatZshCompletionInstall(install));
12441
+ }).pipe(Effect.provide(layer$53))))]));
12442
+ function formatZshCompletionInstall(install) {
12443
+ const installed = `Installed zsh completions at ${install.path}.`;
12444
+ if (!install.requiresFpathEntry) return `${installed} Open a new shell to use them.`;
12445
+ return [
12446
+ installed,
12447
+ `Add this to ~/.zshrc before compinit runs, then open a new shell:`,
12448
+ ` fpath=(${install.path.replace(/\/[^/]+$/u, "")} $fpath)`
12449
+ ].join("\n");
12450
+ }
12451
+ //#endregion
12033
12452
  //#region src/orchestration/Services/OrchestrationEngine.ts
12034
12453
  /**
12035
12454
  * OrchestrationEngineService - Service tag for orchestration engine access.
@@ -16543,273 +16962,6 @@ function mergeGitStatusParts(local, remote) {
16543
16962
  };
16544
16963
  }
16545
16964
  //#endregion
16546
- //#region src/stream/collectUint8StreamText.ts
16547
- const collectUint8StreamText = (input) => {
16548
- const maxBytes = input.maxBytes ?? Number.POSITIVE_INFINITY;
16549
- const truncatedMarker = input.truncatedMarker ?? "";
16550
- return input.stream.pipe(Stream.runFold(() => ({
16551
- chunks: [],
16552
- bytes: 0,
16553
- truncated: false
16554
- }), (state, chunk) => {
16555
- if (state.truncated) return state;
16556
- const remainingBytes = maxBytes - state.bytes;
16557
- if (remainingBytes <= 0) return {
16558
- ...state,
16559
- truncated: true
16560
- };
16561
- const nextChunk = chunk.byteLength > remainingBytes ? chunk.slice(0, remainingBytes) : chunk;
16562
- state.chunks.push(nextChunk);
16563
- const bytes = state.bytes + nextChunk.byteLength;
16564
- const truncated = chunk.byteLength > remainingBytes;
16565
- return {
16566
- chunks: state.chunks,
16567
- bytes,
16568
- truncated
16569
- };
16570
- }), Effect.map((state) => {
16571
- const text = Buffer.concat(state.chunks, state.bytes).toString("utf8");
16572
- return {
16573
- text: state.truncated && truncatedMarker.length > 0 ? `${text}${truncatedMarker}` : text,
16574
- bytes: state.bytes,
16575
- truncated: state.truncated
16576
- };
16577
- }));
16578
- };
16579
- //#endregion
16580
- //#region src/processRunner.ts
16581
- const ProcessInvocationFields = {
16582
- command: Schema$1.String,
16583
- argumentCount: Schema$1.Number,
16584
- cwd: Schema$1.optional(Schema$1.String),
16585
- spawnCwd: Schema$1.optional(Schema$1.String)
16586
- };
16587
- const formatProcessInvocation = (input) => {
16588
- const executionCwd = input.spawnCwd ?? input.cwd;
16589
- return executionCwd === void 0 ? `'${input.command}'` : `'${input.command}' in '${executionCwd}'`;
16590
- };
16591
- var ProcessSpawnError = class extends Schema$1.TaggedErrorClass()("ProcessSpawnError", {
16592
- ...ProcessInvocationFields,
16593
- resolvedCommand: Schema$1.optional(Schema$1.String),
16594
- resolvedArgumentCount: Schema$1.optional(Schema$1.Number),
16595
- shell: Schema$1.optional(Schema$1.Boolean),
16596
- cause: Schema$1.Defect()
16597
- }) {
16598
- get message() {
16599
- return `Failed to spawn process ${formatProcessInvocation(this)}`;
16600
- }
16601
- };
16602
- var ProcessStdinError = class extends Schema$1.TaggedErrorClass()("ProcessStdinError", {
16603
- ...ProcessInvocationFields,
16604
- stdinBytes: Schema$1.Number,
16605
- cause: Schema$1.Defect()
16606
- }) {
16607
- get message() {
16608
- return `Failed to write stdin for process ${formatProcessInvocation(this)}`;
16609
- }
16610
- };
16611
- var ProcessOutputLimitError = class extends Schema$1.TaggedErrorClass()("ProcessOutputLimitError", {
16612
- ...ProcessInvocationFields,
16613
- stream: Schema$1.Literals(["stdout", "stderr"]),
16614
- maxBytes: Schema$1.Number,
16615
- observedBytes: Schema$1.Number
16616
- }) {
16617
- get message() {
16618
- return `Process ${formatProcessInvocation(this)} ${this.stream} produced ${this.observedBytes} bytes, exceeding the ${this.maxBytes} byte limit`;
16619
- }
16620
- };
16621
- var ProcessReadError = class extends Schema$1.TaggedErrorClass()("ProcessReadError", {
16622
- ...ProcessInvocationFields,
16623
- stream: Schema$1.Literals([
16624
- "stdout",
16625
- "stderr",
16626
- "exitCode"
16627
- ]),
16628
- cause: Schema$1.Defect()
16629
- }) {
16630
- get message() {
16631
- return `Failed to read ${this.stream} for process ${formatProcessInvocation(this)}`;
16632
- }
16633
- };
16634
- var ProcessTimeoutError = class extends Schema$1.TaggedErrorClass()("ProcessTimeoutError", {
16635
- ...ProcessInvocationFields,
16636
- timeoutMs: Schema$1.Number
16637
- }) {
16638
- get message() {
16639
- return `Process ${formatProcessInvocation(this)} timed out after ${this.timeoutMs}ms`;
16640
- }
16641
- };
16642
- Schema$1.Union([
16643
- ProcessSpawnError,
16644
- ProcessStdinError,
16645
- ProcessOutputLimitError,
16646
- ProcessReadError,
16647
- ProcessTimeoutError
16648
- ]);
16649
- var ProcessRunner = class extends Context.Service()("@p4code/cli/processRunner") {};
16650
- const DEFAULT_TIMEOUT = "60 seconds";
16651
- const DEFAULT_MAX_OUTPUT_BYTES$2 = 8 * 1024 * 1024;
16652
- const WINDOWS_COMMAND_NOT_FOUND_PATTERNS = [
16653
- /is not recognized as an internal or external command/i,
16654
- /n.o . reconhecido como um comando interno/i,
16655
- /non . riconosciuto come comando interno o esterno/i,
16656
- /n.est pas reconnu en tant que commande interne/i,
16657
- /no se reconoce como un comando interno o externo/i,
16658
- /wird nicht als interner oder externer befehl/i
16659
- ];
16660
- function hasWindowsCommandNotFoundMessage(output) {
16661
- return WINDOWS_COMMAND_NOT_FOUND_PATTERNS.some((pattern) => pattern.test(output));
16662
- }
16663
- const isWindowsCommandNotFound = Effect.fn("processRunner.isWindowsCommandNotFound")(function* (code, stderr) {
16664
- if ((yield* HostProcessPlatform) !== "win32") return false;
16665
- if (code === 9009) return true;
16666
- return hasWindowsCommandNotFoundMessage(stderr);
16667
- });
16668
- const collectText = Effect.fn("processRunner.collectText")(function* (input) {
16669
- const stream = input.stream.pipe(Stream.mapError((cause) => new ProcessReadError({
16670
- command: input.command,
16671
- argumentCount: input.args.length,
16672
- cwd: input.cwd,
16673
- spawnCwd: input.spawnCwd,
16674
- stream: input.streamName,
16675
- cause
16676
- })));
16677
- if (input.outputMode === "truncate") return yield* collectUint8StreamText({
16678
- stream,
16679
- maxBytes: input.maxOutputBytes,
16680
- truncatedMarker: input.truncatedMarker
16681
- });
16682
- return yield* stream.pipe(Stream.runFoldEffect(() => ({
16683
- chunks: [],
16684
- bytes: 0
16685
- }), (state, chunk) => {
16686
- const remainingBytes = input.maxOutputBytes - state.bytes;
16687
- if (chunk.byteLength > remainingBytes) return Effect.fail(new ProcessOutputLimitError({
16688
- command: input.command,
16689
- argumentCount: input.args.length,
16690
- cwd: input.cwd,
16691
- spawnCwd: input.spawnCwd,
16692
- stream: input.streamName,
16693
- maxBytes: input.maxOutputBytes,
16694
- observedBytes: state.bytes + chunk.byteLength
16695
- }));
16696
- state.chunks.push(chunk);
16697
- return Effect.succeed({
16698
- chunks: state.chunks,
16699
- bytes: state.bytes + chunk.byteLength
16700
- });
16701
- }), Effect.map((state) => ({
16702
- text: Buffer.concat(state.chunks, state.bytes).toString("utf8"),
16703
- bytes: state.bytes,
16704
- truncated: false
16705
- })));
16706
- });
16707
- function finalizeRunProcess(effect, input) {
16708
- const timeout = Duration.fromInputUnsafe(input.timeout ?? DEFAULT_TIMEOUT);
16709
- const timeoutBehavior = input.timeoutBehavior ?? "error";
16710
- return effect.pipe(Effect.scoped, Effect.timeoutOption(timeout), Effect.flatMap((result) => {
16711
- if (Option.isSome(result)) return Effect.succeed(result.value);
16712
- if (timeoutBehavior === "timedOutResult") return Effect.succeed({
16713
- stdout: "",
16714
- stderr: "",
16715
- code: null,
16716
- timedOut: true,
16717
- stdoutTruncated: false,
16718
- stderrTruncated: false
16719
- });
16720
- return Effect.fail(new ProcessTimeoutError({
16721
- command: input.command,
16722
- argumentCount: input.args.length,
16723
- cwd: input.cwd,
16724
- spawnCwd: input.spawnCwd,
16725
- timeoutMs: Duration.toMillis(timeout)
16726
- }));
16727
- }));
16728
- }
16729
- const runProcessCore = Effect.fn("processRunner.runProcessCore")(function* (spawner, input) {
16730
- const maxOutputBytes = input.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES$2;
16731
- const outputMode = input.outputMode ?? "error";
16732
- const truncatedMarker = input.truncatedMarker ?? "";
16733
- const extendEnv = input.env !== void 0;
16734
- const spawnCommand = yield* resolveSpawnCommand(input.command, input.args, input.env === void 0 ? {} : {
16735
- env: input.env,
16736
- extendEnv
16737
- });
16738
- const child = yield* spawner.spawn(ChildProcess$1.make(spawnCommand.command, spawnCommand.args, {
16739
- ...input.spawnCwd ?? input.cwd ? { cwd: input.spawnCwd ?? input.cwd } : {},
16740
- ...input.env !== void 0 ? {
16741
- env: input.env,
16742
- extendEnv
16743
- } : {},
16744
- shell: spawnCommand.shell
16745
- })).pipe(Effect.mapError((cause) => new ProcessSpawnError({
16746
- command: input.command,
16747
- argumentCount: input.args.length,
16748
- cwd: input.cwd,
16749
- spawnCwd: input.spawnCwd,
16750
- resolvedCommand: spawnCommand.command,
16751
- resolvedArgumentCount: spawnCommand.args.length,
16752
- shell: spawnCommand.shell,
16753
- cause
16754
- })));
16755
- const stdin = input.stdin;
16756
- const writeStdin = stdin === void 0 ? Effect.void : Stream.run(Stream.encodeText(Stream.make(stdin)), child.stdin).pipe(Effect.mapError((cause) => new ProcessStdinError({
16757
- command: input.command,
16758
- argumentCount: input.args.length,
16759
- cwd: input.cwd,
16760
- spawnCwd: input.spawnCwd,
16761
- stdinBytes: Buffer.byteLength(stdin),
16762
- cause
16763
- })));
16764
- const [stdout, stderr] = yield* Effect.all([
16765
- collectText({
16766
- command: input.command,
16767
- args: input.args,
16768
- cwd: input.cwd,
16769
- spawnCwd: input.spawnCwd,
16770
- streamName: "stdout",
16771
- stream: child.stdout,
16772
- maxOutputBytes,
16773
- outputMode,
16774
- truncatedMarker
16775
- }),
16776
- collectText({
16777
- command: input.command,
16778
- args: input.args,
16779
- cwd: input.cwd,
16780
- spawnCwd: input.spawnCwd,
16781
- streamName: "stderr",
16782
- stream: child.stderr,
16783
- maxOutputBytes,
16784
- outputMode,
16785
- truncatedMarker
16786
- }),
16787
- writeStdin
16788
- ], { concurrency: "unbounded" });
16789
- const exitCode = yield* child.exitCode.pipe(Effect.mapError((cause) => new ProcessReadError({
16790
- command: input.command,
16791
- argumentCount: input.args.length,
16792
- cwd: input.cwd,
16793
- spawnCwd: input.spawnCwd,
16794
- stream: "exitCode",
16795
- cause
16796
- })));
16797
- return {
16798
- stdout: stdout.text,
16799
- stderr: stderr.text,
16800
- code: exitCode,
16801
- timedOut: false,
16802
- stdoutTruncated: stdout.truncated,
16803
- stderrTruncated: stderr.truncated
16804
- };
16805
- });
16806
- const make$58 = Effect.fn("ProcessRunner.make")(function* () {
16807
- const spawner = yield* ChildProcessSpawner$1.ChildProcessSpawner;
16808
- const run = (input) => finalizeRunProcess(runProcessCore(spawner, input), input);
16809
- return ProcessRunner.of({ run });
16810
- });
16811
- const layer$53 = Layer.effect(ProcessRunner, make$58());
16812
- //#endregion
16813
16965
  //#region src/project/RepositoryIdentityResolver.ts
16814
16966
  const DEFAULT_REPOSITORY_IDENTITY_CACHE_CAPACITY = 512;
16815
16967
  const DEFAULT_POSITIVE_CACHE_TTL = Duration.minutes(1);
@@ -78243,104 +78395,6 @@ const serveCommand = Command.make("serve", { ...sharedServerCommandFlags }).pipe
78243
78395
  forceAutoBootstrapProjectFromCwd: false
78244
78396
  })));
78245
78397
  //#endregion
78246
- //#region src/cli/completions.ts
78247
- /**
78248
- * Installs the zsh completion script for the `p4` command.
78249
- *
78250
- * zsh ships `_perforce` with `#compdef p4 p4d`, so out of the box tabbing
78251
- * through our subcommands prints "unhandled perforce command: …" from its
78252
- * fallback message. `compinit` resolves duplicate `#compdef` claims by fpath
78253
- * order — first match wins — and `_perforce` lives in the *last* fpath entry
78254
- * (`/usr/share/zsh/<ver>/functions`). Writing our own `_p4` into any earlier
78255
- * directory therefore replaces it outright, with no shell-config edit.
78256
- */
78257
- const COMPLETION_FILE = "_p4";
78258
- const GENERATE_TIMEOUT_MS = 1e4;
78259
- /**
78260
- * Directories ahead of `/usr/share/zsh/*` in a default fpath, most specific
78261
- * first. `~/.zfunc` is the fallback and is the only one needing an fpath line.
78262
- */
78263
- function candidateDirectories(input) {
78264
- return [
78265
- {
78266
- dir: input.join(input.homeDir, ".oh-my-zsh", "custom", "completions"),
78267
- requiresFpathEntry: false
78268
- },
78269
- {
78270
- dir: "/opt/homebrew/share/zsh/site-functions",
78271
- requiresFpathEntry: false
78272
- },
78273
- {
78274
- dir: "/usr/local/share/zsh/site-functions",
78275
- requiresFpathEntry: false
78276
- },
78277
- {
78278
- dir: input.join(input.homeDir, ".zfunc"),
78279
- requiresFpathEntry: true
78280
- }
78281
- ];
78282
- }
78283
- /**
78284
- * Asks the running CLI for its own completion script rather than rendering one
78285
- * here, so the output cannot drift from what `--completions` produces.
78286
- */
78287
- const generateZshCompletionScript = Effect.fn("cli.completions.generate")(function* () {
78288
- const runner = yield* ProcessRunner;
78289
- const execPath = yield* HostProcessExecutablePath;
78290
- const entryPath = (yield* HostProcessArguments)[1] ?? "";
78291
- if (entryPath === "") return null;
78292
- const result = yield* runner.run({
78293
- command: execPath,
78294
- args: [
78295
- entryPath,
78296
- "--completions",
78297
- "zsh"
78298
- ],
78299
- timeout: GENERATE_TIMEOUT_MS
78300
- });
78301
- if (result.code !== 0) return null;
78302
- const script = result.stdout.trim();
78303
- return script.length === 0 ? null : `${script}\n`;
78304
- });
78305
- /**
78306
- * Best-effort: returns null when completions are not applicable (Windows, no
78307
- * writable directory) rather than failing the command that asked for them.
78308
- * Installing completions is never the reason an install should fail.
78309
- */
78310
- const installZshCompletion = Effect.fn("cli.completions.install")(function* () {
78311
- if ((yield* HostProcessPlatform) === "win32") return null;
78312
- const fs = yield* FileSystem.FileSystem;
78313
- const path = yield* Path.Path;
78314
- const homeDir = (yield* HostProcessEnvironment).HOME ?? "";
78315
- if (homeDir === "") return null;
78316
- const script = yield* generateZshCompletionScript();
78317
- if (script === null) return null;
78318
- for (const candidate of candidateDirectories({
78319
- homeDir,
78320
- join: path.join
78321
- })) {
78322
- if (candidate.requiresFpathEntry) {
78323
- if (!(yield* fs.makeDirectory(candidate.dir, { recursive: true }).pipe(Effect.as(true), Effect.orElseSucceed(() => false)))) continue;
78324
- } else if (!(yield* fs.exists(candidate.dir).pipe(Effect.orElseSucceed(() => false)))) continue;
78325
- const target = path.join(candidate.dir, COMPLETION_FILE);
78326
- if (!(yield* fs.writeFileString(target, script).pipe(Effect.as(true), Effect.orElseSucceed(() => false)))) continue;
78327
- return {
78328
- path: target,
78329
- requiresFpathEntry: candidate.requiresFpathEntry
78330
- };
78331
- }
78332
- return null;
78333
- });
78334
- function formatZshCompletionInstall(install) {
78335
- const installed = `Installed zsh completions at ${install.path}.`;
78336
- if (!install.requiresFpathEntry) return `${installed} Open a new shell to use them.`;
78337
- return [
78338
- installed,
78339
- `Add this to ~/.zshrc before compinit runs, then open a new shell:`,
78340
- ` fpath=(${install.path.replace(/\/[^/]+$/u, "")} $fpath)`
78341
- ].join("\n");
78342
- }
78343
- //#endregion
78344
78398
  //#region src/cli/service.ts
78345
78399
  const isBootServiceCommandError = Schema$1.is(BootServiceCommandError);
78346
78400
  const bootServiceLayer = (config) => layer$46({
@@ -78395,10 +78449,21 @@ const runServiceCommand = Effect.fn("cli.service.run")(function* (flags, run) {
78395
78449
  if (baseDirNotice !== null) yield* Console.error(baseDirNotice);
78396
78450
  return yield* run.pipe(Effect.tapError((error) => isBootServiceCommandError(error) ? BootService.pipe(Effect.flatMap((service) => Console.error(`Details: ${service.logPath}`))) : Effect.void), Effect.provide(bootServiceLayer(config)), Effect.provide(layer$53));
78397
78451
  });
78452
+ /**
78453
+ * Runs on every `service install` and `service update`, including the
78454
+ * already-current paths. Gating it on "the unit changed" meant a host whose
78455
+ * service was already installed — the common case after the first setup — never
78456
+ * got completions at all.
78457
+ */
78458
+ const reportZshCompletionInstall = Effect.gen(function* () {
78459
+ const completion = yield* installZshCompletion().pipe(Effect.orElseSucceed(() => null));
78460
+ if (completion !== null) yield* Console.log(formatZshCompletionInstall(completion));
78461
+ });
78398
78462
  const serviceInstallCommand = Command.make("install", projectLocationFlags).pipe(Command.withDescription("Install P4Code as a background service for this user."), Command.withHandler((flags) => runServiceCommand(flags, Effect.gen(function* () {
78399
78463
  const result = yield* reconcileService();
78400
78464
  if (!result.changed) {
78401
78465
  yield* Console.log(`P4Code service is already installed with @p4code/cli@${version}.`);
78466
+ yield* reportZshCompletionInstall;
78402
78467
  return;
78403
78468
  }
78404
78469
  yield* Console.log(formatReconcileSuccess({
@@ -78407,14 +78472,14 @@ const serviceInstallCommand = Command.make("install", projectLocationFlags).pipe
78407
78472
  cliVersion: version,
78408
78473
  platform: yield* HostProcessPlatform
78409
78474
  }));
78410
- const completion = yield* installZshCompletion().pipe(Effect.orElseSucceed(() => null));
78411
- if (completion !== null) yield* Console.log(formatZshCompletionInstall(completion));
78475
+ yield* reportZshCompletionInstall;
78412
78476
  }))));
78413
78477
  const serviceUpdateCommand = Command.make("update", projectLocationFlags).pipe(Command.withDescription("Update or repair the background service so it runs this CLI build, then restart it."), Command.withHandler((flags) => runServiceCommand(flags, Effect.gen(function* () {
78414
78478
  const result = yield* reconcileService();
78415
78479
  if (!result.changed) {
78416
78480
  yield* (yield* BootService).restart;
78417
78481
  yield* Console.log(`Restarted the P4Code service on @p4code/cli@${version}.`);
78482
+ yield* reportZshCompletionInstall;
78418
78483
  return;
78419
78484
  }
78420
78485
  yield* Console.log(formatReconcileSuccess({
@@ -78423,6 +78488,7 @@ const serviceUpdateCommand = Command.make("update", projectLocationFlags).pipe(C
78423
78488
  cliVersion: version,
78424
78489
  platform: yield* HostProcessPlatform
78425
78490
  }));
78491
+ yield* reportZshCompletionInstall;
78426
78492
  }))));
78427
78493
  const serviceUninstallCommand = Command.make("uninstall", projectLocationFlags).pipe(Command.withDescription("Stop and remove the P4Code background service."), Command.withHandler((flags) => runServiceCommand(flags, Effect.gen(function* () {
78428
78494
  const removed = yield* (yield* BootService).uninstall;
@@ -78471,11 +78537,14 @@ const reportUpdateAvailable = Effect.gen(function* () {
78471
78537
  stateDir: paths.stateDir
78472
78538
  });
78473
78539
  if (reminder !== null) yield* Console.error(reminder);
78540
+ const completionsHint = yield* resolveMissingCompletionsHint({ stateDir: paths.stateDir });
78541
+ if (completionsHint !== null) yield* Console.error(completionsHint);
78474
78542
  }).pipe(Effect.ignore);
78475
78543
  const makeCli = () => Command.make("p4", { ...sharedServerCommandFlags }).pipe(Command.withDescription("Run the P4Code server."), Command.withHandler((flags) => runServerCommand(flags)), Command.withSubcommands([
78476
78544
  startCommand,
78477
78545
  serveCommand,
78478
78546
  authCommand,
78547
+ completionsCommand,
78479
78548
  projectCommand,
78480
78549
  serviceCommand
78481
78550
  ]));