@p4code/cli 0.0.37 → 0.0.39

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.37";
235
+ var version = "0.0.39";
236
236
  //#endregion
237
237
  //#region src/config.ts
238
238
  /**
@@ -10840,7 +10840,7 @@ Layer.effectDiscard(runMigrations());
10840
10840
  //#region src/persistence/Layers/Sqlite.ts
10841
10841
  const defaultSqliteClientLoaders = {
10842
10842
  bun: () => import("@effect/sql-sqlite-bun/SqliteClient"),
10843
- node: () => import("./NodeSqliteClient-BUlNQAY0.mjs")
10843
+ node: () => import("./NodeSqliteClient-Dq296EBh.mjs")
10844
10844
  };
10845
10845
  const makeRuntimeSqliteLayer = Effect.fn("makeRuntimeSqliteLayer")(function* (config) {
10846
10846
  const runtime = process.versions.bun !== void 0 ? "bun" : "node";
@@ -12035,6 +12035,422 @@ const sessionCommand = Command.make("session").pipe(Command.withDescription("Man
12035
12035
  ]));
12036
12036
  const authCommand = Command.make("auth").pipe(Command.withDescription("Manage the local auth control plane for headless deployments."), Command.withSubcommands([pairingCommand, sessionCommand]));
12037
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
+ * Ordered by how reliably zsh actually scans them.
12320
+ *
12321
+ * oh-my-zsh comes first and is created on demand: it adds
12322
+ * `custom/completions` to fpath unconditionally, it is user-owned so
12323
+ * `compinit -i` never skips it as insecure, and it does not depend on a package
12324
+ * manager's prefix. The Homebrew and /usr/local directories are only used when
12325
+ * they already exist, and even then are not guaranteed to be on fpath —
12326
+ * `brew shellenv` contributes Homebrew's, and a plugin that rebuilds fpath can
12327
+ * drop it, which is exactly how a completion file lands somewhere zsh never
12328
+ * reads.
12329
+ */
12330
+ function candidateDirectories(input) {
12331
+ return [
12332
+ {
12333
+ dir: input.join(input.homeDir, ".oh-my-zsh", "custom", "completions"),
12334
+ createWhenPresent: input.join(input.homeDir, ".oh-my-zsh")
12335
+ },
12336
+ { dir: "/opt/homebrew/share/zsh/site-functions" },
12337
+ { dir: "/usr/local/share/zsh/site-functions" },
12338
+ {
12339
+ dir: input.join(input.homeDir, ".zfunc"),
12340
+ requiresFpathEntry: true
12341
+ }
12342
+ ];
12343
+ }
12344
+ /**
12345
+ * Asks the running CLI for its own completion script rather than rendering one
12346
+ * here, so the output cannot drift from what `--completions` produces.
12347
+ */
12348
+ const generateZshCompletionScript = Effect.fn("cli.completions.generate")(function* () {
12349
+ const runner = yield* ProcessRunner;
12350
+ const execPath = yield* HostProcessExecutablePath;
12351
+ const entryPath = (yield* HostProcessArguments)[1] ?? "";
12352
+ if (entryPath === "") return null;
12353
+ const result = yield* runner.run({
12354
+ command: execPath,
12355
+ args: [
12356
+ entryPath,
12357
+ "--completions",
12358
+ "zsh"
12359
+ ],
12360
+ timeout: GENERATE_TIMEOUT_MS
12361
+ });
12362
+ if (result.code !== 0) return null;
12363
+ const script = result.stdout.trim();
12364
+ return script.length === 0 ? null : `${script}\n`;
12365
+ });
12366
+ /**
12367
+ * Best-effort: returns null when completions are not applicable (Windows, no
12368
+ * writable directory) rather than failing the command that asked for them.
12369
+ * Installing completions is never the reason an install should fail.
12370
+ */
12371
+ const installZshCompletion = Effect.fn("cli.completions.install")(function* () {
12372
+ if ((yield* HostProcessPlatform) === "win32") return null;
12373
+ const fs = yield* FileSystem.FileSystem;
12374
+ const path = yield* Path.Path;
12375
+ const homeDir = (yield* HostProcessEnvironment).HOME ?? "";
12376
+ if (homeDir === "") return null;
12377
+ const script = yield* generateZshCompletionScript();
12378
+ if (script === null) return null;
12379
+ for (const candidate of candidateDirectories({
12380
+ homeDir,
12381
+ join: path.join
12382
+ })) {
12383
+ if (candidate.requiresFpathEntry === true || candidate.createWhenPresent !== void 0 && (yield* fs.exists(candidate.createWhenPresent).pipe(Effect.orElseSucceed(() => false)))) {
12384
+ if (!(yield* fs.makeDirectory(candidate.dir, { recursive: true }).pipe(Effect.as(true), Effect.orElseSucceed(() => false)))) continue;
12385
+ } else if (!(yield* fs.exists(candidate.dir).pipe(Effect.orElseSucceed(() => false)))) continue;
12386
+ const target = path.join(candidate.dir, COMPLETION_FILE);
12387
+ if (!(yield* fs.writeFileString(target, script).pipe(Effect.as(true), Effect.orElseSucceed(() => false)))) continue;
12388
+ return {
12389
+ path: target,
12390
+ requiresFpathEntry: candidate.requiresFpathEntry === true
12391
+ };
12392
+ }
12393
+ return null;
12394
+ });
12395
+ const HINT_MARKER_FILE = "completions-hint";
12396
+ const HINT_INTERVAL_MS = 1440 * 60 * 1e3;
12397
+ const COMPLETIONS_HINT = "Shell completions are not installed, so tab completion shows Perforce's. Run `p4 completions install`.";
12398
+ /**
12399
+ * A hint when no `_p4` is on the fpath, at most once a day.
12400
+ *
12401
+ * A stat of a few directories, deliberately not the install itself: generating
12402
+ * the script spawns a subprocess, which is far too much to do on every command
12403
+ * just in case. Throttled through a marker file so an unactioned hint does not
12404
+ * become noise on every invocation.
12405
+ */
12406
+ const resolveMissingCompletionsHint = Effect.fn("cli.completions.hint")(function* (input) {
12407
+ if ((yield* HostProcessPlatform) === "win32") return null;
12408
+ const fs = yield* FileSystem.FileSystem;
12409
+ const path = yield* Path.Path;
12410
+ const homeDir = (yield* HostProcessEnvironment).HOME ?? "";
12411
+ if (homeDir === "") return null;
12412
+ for (const candidate of candidateDirectories({
12413
+ homeDir,
12414
+ join: path.join
12415
+ })) if (yield* fs.exists(path.join(candidate.dir, COMPLETION_FILE)).pipe(Effect.orElseSucceed(() => false))) return null;
12416
+ const markerPath = path.join(input.stateDir, HINT_MARKER_FILE);
12417
+ const marker = yield* fs.stat(markerPath).pipe(Effect.asSome, Effect.orElseSucceed(() => null));
12418
+ const nowMs = DateTime.toEpochMillis(yield* DateTime.now);
12419
+ if (marker !== null && Option.isSome(marker)) {
12420
+ const mtime = marker.value.mtime;
12421
+ if (Option.isSome(mtime) && nowMs - mtime.value.getTime() < HINT_INTERVAL_MS) return null;
12422
+ }
12423
+ yield* fs.makeDirectory(input.stateDir, { recursive: true }).pipe(Effect.ignore);
12424
+ yield* fs.writeFileString(markerPath, "").pipe(Effect.ignore);
12425
+ return COMPLETIONS_HINT;
12426
+ });
12427
+ /**
12428
+ * Standalone so completions never depend on the service. A laptop that only
12429
+ * runs clients has no service to install, and a host whose `service install`
12430
+ * fails still deserves working completions.
12431
+ */
12432
+ 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* () {
12433
+ const install = yield* installZshCompletion();
12434
+ if (install === null) {
12435
+ yield* Console.error([
12436
+ "Could not install completions automatically.",
12437
+ "Write them yourself with:",
12438
+ " p4 --completions zsh > <a directory on your fpath>/_p4"
12439
+ ].join("\n"));
12440
+ return;
12441
+ }
12442
+ yield* Console.log(formatZshCompletionInstall(install));
12443
+ }).pipe(Effect.provide(layer$53))))]));
12444
+ function formatZshCompletionInstall(install) {
12445
+ const installed = `Installed zsh completions at ${install.path}.`;
12446
+ if (!install.requiresFpathEntry) return `${installed} Open a new shell to use them.`;
12447
+ return [
12448
+ installed,
12449
+ `Add this to ~/.zshrc before compinit runs, then open a new shell:`,
12450
+ ` fpath=(${install.path.replace(/\/[^/]+$/u, "")} $fpath)`
12451
+ ].join("\n");
12452
+ }
12453
+ //#endregion
12038
12454
  //#region src/orchestration/Services/OrchestrationEngine.ts
12039
12455
  /**
12040
12456
  * OrchestrationEngineService - Service tag for orchestration engine access.
@@ -16548,273 +16964,6 @@ function mergeGitStatusParts(local, remote) {
16548
16964
  };
16549
16965
  }
16550
16966
  //#endregion
16551
- //#region src/stream/collectUint8StreamText.ts
16552
- const collectUint8StreamText = (input) => {
16553
- const maxBytes = input.maxBytes ?? Number.POSITIVE_INFINITY;
16554
- const truncatedMarker = input.truncatedMarker ?? "";
16555
- return input.stream.pipe(Stream.runFold(() => ({
16556
- chunks: [],
16557
- bytes: 0,
16558
- truncated: false
16559
- }), (state, chunk) => {
16560
- if (state.truncated) return state;
16561
- const remainingBytes = maxBytes - state.bytes;
16562
- if (remainingBytes <= 0) return {
16563
- ...state,
16564
- truncated: true
16565
- };
16566
- const nextChunk = chunk.byteLength > remainingBytes ? chunk.slice(0, remainingBytes) : chunk;
16567
- state.chunks.push(nextChunk);
16568
- const bytes = state.bytes + nextChunk.byteLength;
16569
- const truncated = chunk.byteLength > remainingBytes;
16570
- return {
16571
- chunks: state.chunks,
16572
- bytes,
16573
- truncated
16574
- };
16575
- }), Effect.map((state) => {
16576
- const text = Buffer.concat(state.chunks, state.bytes).toString("utf8");
16577
- return {
16578
- text: state.truncated && truncatedMarker.length > 0 ? `${text}${truncatedMarker}` : text,
16579
- bytes: state.bytes,
16580
- truncated: state.truncated
16581
- };
16582
- }));
16583
- };
16584
- //#endregion
16585
- //#region src/processRunner.ts
16586
- const ProcessInvocationFields = {
16587
- command: Schema$1.String,
16588
- argumentCount: Schema$1.Number,
16589
- cwd: Schema$1.optional(Schema$1.String),
16590
- spawnCwd: Schema$1.optional(Schema$1.String)
16591
- };
16592
- const formatProcessInvocation = (input) => {
16593
- const executionCwd = input.spawnCwd ?? input.cwd;
16594
- return executionCwd === void 0 ? `'${input.command}'` : `'${input.command}' in '${executionCwd}'`;
16595
- };
16596
- var ProcessSpawnError = class extends Schema$1.TaggedErrorClass()("ProcessSpawnError", {
16597
- ...ProcessInvocationFields,
16598
- resolvedCommand: Schema$1.optional(Schema$1.String),
16599
- resolvedArgumentCount: Schema$1.optional(Schema$1.Number),
16600
- shell: Schema$1.optional(Schema$1.Boolean),
16601
- cause: Schema$1.Defect()
16602
- }) {
16603
- get message() {
16604
- return `Failed to spawn process ${formatProcessInvocation(this)}`;
16605
- }
16606
- };
16607
- var ProcessStdinError = class extends Schema$1.TaggedErrorClass()("ProcessStdinError", {
16608
- ...ProcessInvocationFields,
16609
- stdinBytes: Schema$1.Number,
16610
- cause: Schema$1.Defect()
16611
- }) {
16612
- get message() {
16613
- return `Failed to write stdin for process ${formatProcessInvocation(this)}`;
16614
- }
16615
- };
16616
- var ProcessOutputLimitError = class extends Schema$1.TaggedErrorClass()("ProcessOutputLimitError", {
16617
- ...ProcessInvocationFields,
16618
- stream: Schema$1.Literals(["stdout", "stderr"]),
16619
- maxBytes: Schema$1.Number,
16620
- observedBytes: Schema$1.Number
16621
- }) {
16622
- get message() {
16623
- return `Process ${formatProcessInvocation(this)} ${this.stream} produced ${this.observedBytes} bytes, exceeding the ${this.maxBytes} byte limit`;
16624
- }
16625
- };
16626
- var ProcessReadError = class extends Schema$1.TaggedErrorClass()("ProcessReadError", {
16627
- ...ProcessInvocationFields,
16628
- stream: Schema$1.Literals([
16629
- "stdout",
16630
- "stderr",
16631
- "exitCode"
16632
- ]),
16633
- cause: Schema$1.Defect()
16634
- }) {
16635
- get message() {
16636
- return `Failed to read ${this.stream} for process ${formatProcessInvocation(this)}`;
16637
- }
16638
- };
16639
- var ProcessTimeoutError = class extends Schema$1.TaggedErrorClass()("ProcessTimeoutError", {
16640
- ...ProcessInvocationFields,
16641
- timeoutMs: Schema$1.Number
16642
- }) {
16643
- get message() {
16644
- return `Process ${formatProcessInvocation(this)} timed out after ${this.timeoutMs}ms`;
16645
- }
16646
- };
16647
- Schema$1.Union([
16648
- ProcessSpawnError,
16649
- ProcessStdinError,
16650
- ProcessOutputLimitError,
16651
- ProcessReadError,
16652
- ProcessTimeoutError
16653
- ]);
16654
- var ProcessRunner = class extends Context.Service()("@p4code/cli/processRunner") {};
16655
- const DEFAULT_TIMEOUT = "60 seconds";
16656
- const DEFAULT_MAX_OUTPUT_BYTES$2 = 8 * 1024 * 1024;
16657
- const WINDOWS_COMMAND_NOT_FOUND_PATTERNS = [
16658
- /is not recognized as an internal or external command/i,
16659
- /n.o . reconhecido como um comando interno/i,
16660
- /non . riconosciuto come comando interno o esterno/i,
16661
- /n.est pas reconnu en tant que commande interne/i,
16662
- /no se reconoce como un comando interno o externo/i,
16663
- /wird nicht als interner oder externer befehl/i
16664
- ];
16665
- function hasWindowsCommandNotFoundMessage(output) {
16666
- return WINDOWS_COMMAND_NOT_FOUND_PATTERNS.some((pattern) => pattern.test(output));
16667
- }
16668
- const isWindowsCommandNotFound = Effect.fn("processRunner.isWindowsCommandNotFound")(function* (code, stderr) {
16669
- if ((yield* HostProcessPlatform) !== "win32") return false;
16670
- if (code === 9009) return true;
16671
- return hasWindowsCommandNotFoundMessage(stderr);
16672
- });
16673
- const collectText = Effect.fn("processRunner.collectText")(function* (input) {
16674
- const stream = input.stream.pipe(Stream.mapError((cause) => new ProcessReadError({
16675
- command: input.command,
16676
- argumentCount: input.args.length,
16677
- cwd: input.cwd,
16678
- spawnCwd: input.spawnCwd,
16679
- stream: input.streamName,
16680
- cause
16681
- })));
16682
- if (input.outputMode === "truncate") return yield* collectUint8StreamText({
16683
- stream,
16684
- maxBytes: input.maxOutputBytes,
16685
- truncatedMarker: input.truncatedMarker
16686
- });
16687
- return yield* stream.pipe(Stream.runFoldEffect(() => ({
16688
- chunks: [],
16689
- bytes: 0
16690
- }), (state, chunk) => {
16691
- const remainingBytes = input.maxOutputBytes - state.bytes;
16692
- if (chunk.byteLength > remainingBytes) return Effect.fail(new ProcessOutputLimitError({
16693
- command: input.command,
16694
- argumentCount: input.args.length,
16695
- cwd: input.cwd,
16696
- spawnCwd: input.spawnCwd,
16697
- stream: input.streamName,
16698
- maxBytes: input.maxOutputBytes,
16699
- observedBytes: state.bytes + chunk.byteLength
16700
- }));
16701
- state.chunks.push(chunk);
16702
- return Effect.succeed({
16703
- chunks: state.chunks,
16704
- bytes: state.bytes + chunk.byteLength
16705
- });
16706
- }), Effect.map((state) => ({
16707
- text: Buffer.concat(state.chunks, state.bytes).toString("utf8"),
16708
- bytes: state.bytes,
16709
- truncated: false
16710
- })));
16711
- });
16712
- function finalizeRunProcess(effect, input) {
16713
- const timeout = Duration.fromInputUnsafe(input.timeout ?? DEFAULT_TIMEOUT);
16714
- const timeoutBehavior = input.timeoutBehavior ?? "error";
16715
- return effect.pipe(Effect.scoped, Effect.timeoutOption(timeout), Effect.flatMap((result) => {
16716
- if (Option.isSome(result)) return Effect.succeed(result.value);
16717
- if (timeoutBehavior === "timedOutResult") return Effect.succeed({
16718
- stdout: "",
16719
- stderr: "",
16720
- code: null,
16721
- timedOut: true,
16722
- stdoutTruncated: false,
16723
- stderrTruncated: false
16724
- });
16725
- return Effect.fail(new ProcessTimeoutError({
16726
- command: input.command,
16727
- argumentCount: input.args.length,
16728
- cwd: input.cwd,
16729
- spawnCwd: input.spawnCwd,
16730
- timeoutMs: Duration.toMillis(timeout)
16731
- }));
16732
- }));
16733
- }
16734
- const runProcessCore = Effect.fn("processRunner.runProcessCore")(function* (spawner, input) {
16735
- const maxOutputBytes = input.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES$2;
16736
- const outputMode = input.outputMode ?? "error";
16737
- const truncatedMarker = input.truncatedMarker ?? "";
16738
- const extendEnv = input.env !== void 0;
16739
- const spawnCommand = yield* resolveSpawnCommand(input.command, input.args, input.env === void 0 ? {} : {
16740
- env: input.env,
16741
- extendEnv
16742
- });
16743
- const child = yield* spawner.spawn(ChildProcess$1.make(spawnCommand.command, spawnCommand.args, {
16744
- ...input.spawnCwd ?? input.cwd ? { cwd: input.spawnCwd ?? input.cwd } : {},
16745
- ...input.env !== void 0 ? {
16746
- env: input.env,
16747
- extendEnv
16748
- } : {},
16749
- shell: spawnCommand.shell
16750
- })).pipe(Effect.mapError((cause) => new ProcessSpawnError({
16751
- command: input.command,
16752
- argumentCount: input.args.length,
16753
- cwd: input.cwd,
16754
- spawnCwd: input.spawnCwd,
16755
- resolvedCommand: spawnCommand.command,
16756
- resolvedArgumentCount: spawnCommand.args.length,
16757
- shell: spawnCommand.shell,
16758
- cause
16759
- })));
16760
- const stdin = input.stdin;
16761
- const writeStdin = stdin === void 0 ? Effect.void : Stream.run(Stream.encodeText(Stream.make(stdin)), child.stdin).pipe(Effect.mapError((cause) => new ProcessStdinError({
16762
- command: input.command,
16763
- argumentCount: input.args.length,
16764
- cwd: input.cwd,
16765
- spawnCwd: input.spawnCwd,
16766
- stdinBytes: Buffer.byteLength(stdin),
16767
- cause
16768
- })));
16769
- const [stdout, stderr] = yield* Effect.all([
16770
- collectText({
16771
- command: input.command,
16772
- args: input.args,
16773
- cwd: input.cwd,
16774
- spawnCwd: input.spawnCwd,
16775
- streamName: "stdout",
16776
- stream: child.stdout,
16777
- maxOutputBytes,
16778
- outputMode,
16779
- truncatedMarker
16780
- }),
16781
- collectText({
16782
- command: input.command,
16783
- args: input.args,
16784
- cwd: input.cwd,
16785
- spawnCwd: input.spawnCwd,
16786
- streamName: "stderr",
16787
- stream: child.stderr,
16788
- maxOutputBytes,
16789
- outputMode,
16790
- truncatedMarker
16791
- }),
16792
- writeStdin
16793
- ], { concurrency: "unbounded" });
16794
- const exitCode = yield* child.exitCode.pipe(Effect.mapError((cause) => new ProcessReadError({
16795
- command: input.command,
16796
- argumentCount: input.args.length,
16797
- cwd: input.cwd,
16798
- spawnCwd: input.spawnCwd,
16799
- stream: "exitCode",
16800
- cause
16801
- })));
16802
- return {
16803
- stdout: stdout.text,
16804
- stderr: stderr.text,
16805
- code: exitCode,
16806
- timedOut: false,
16807
- stdoutTruncated: stdout.truncated,
16808
- stderrTruncated: stderr.truncated
16809
- };
16810
- });
16811
- const make$58 = Effect.fn("ProcessRunner.make")(function* () {
16812
- const spawner = yield* ChildProcessSpawner$1.ChildProcessSpawner;
16813
- const run = (input) => finalizeRunProcess(runProcessCore(spawner, input), input);
16814
- return ProcessRunner.of({ run });
16815
- });
16816
- const layer$53 = Layer.effect(ProcessRunner, make$58());
16817
- //#endregion
16818
16967
  //#region src/project/RepositoryIdentityResolver.ts
16819
16968
  const DEFAULT_REPOSITORY_IDENTITY_CACHE_CAPACITY = 512;
16820
16969
  const DEFAULT_POSITIVE_CACHE_TTL = Duration.minutes(1);
@@ -78248,104 +78397,6 @@ const serveCommand = Command.make("serve", { ...sharedServerCommandFlags }).pipe
78248
78397
  forceAutoBootstrapProjectFromCwd: false
78249
78398
  })));
78250
78399
  //#endregion
78251
- //#region src/cli/completions.ts
78252
- /**
78253
- * Installs the zsh completion script for the `p4` command.
78254
- *
78255
- * zsh ships `_perforce` with `#compdef p4 p4d`, so out of the box tabbing
78256
- * through our subcommands prints "unhandled perforce command: …" from its
78257
- * fallback message. `compinit` resolves duplicate `#compdef` claims by fpath
78258
- * order — first match wins — and `_perforce` lives in the *last* fpath entry
78259
- * (`/usr/share/zsh/<ver>/functions`). Writing our own `_p4` into any earlier
78260
- * directory therefore replaces it outright, with no shell-config edit.
78261
- */
78262
- const COMPLETION_FILE = "_p4";
78263
- const GENERATE_TIMEOUT_MS = 1e4;
78264
- /**
78265
- * Directories ahead of `/usr/share/zsh/*` in a default fpath, most specific
78266
- * first. `~/.zfunc` is the fallback and is the only one needing an fpath line.
78267
- */
78268
- function candidateDirectories(input) {
78269
- return [
78270
- {
78271
- dir: input.join(input.homeDir, ".oh-my-zsh", "custom", "completions"),
78272
- requiresFpathEntry: false
78273
- },
78274
- {
78275
- dir: "/opt/homebrew/share/zsh/site-functions",
78276
- requiresFpathEntry: false
78277
- },
78278
- {
78279
- dir: "/usr/local/share/zsh/site-functions",
78280
- requiresFpathEntry: false
78281
- },
78282
- {
78283
- dir: input.join(input.homeDir, ".zfunc"),
78284
- requiresFpathEntry: true
78285
- }
78286
- ];
78287
- }
78288
- /**
78289
- * Asks the running CLI for its own completion script rather than rendering one
78290
- * here, so the output cannot drift from what `--completions` produces.
78291
- */
78292
- const generateZshCompletionScript = Effect.fn("cli.completions.generate")(function* () {
78293
- const runner = yield* ProcessRunner;
78294
- const execPath = yield* HostProcessExecutablePath;
78295
- const entryPath = (yield* HostProcessArguments)[1] ?? "";
78296
- if (entryPath === "") return null;
78297
- const result = yield* runner.run({
78298
- command: execPath,
78299
- args: [
78300
- entryPath,
78301
- "--completions",
78302
- "zsh"
78303
- ],
78304
- timeout: GENERATE_TIMEOUT_MS
78305
- });
78306
- if (result.code !== 0) return null;
78307
- const script = result.stdout.trim();
78308
- return script.length === 0 ? null : `${script}\n`;
78309
- });
78310
- /**
78311
- * Best-effort: returns null when completions are not applicable (Windows, no
78312
- * writable directory) rather than failing the command that asked for them.
78313
- * Installing completions is never the reason an install should fail.
78314
- */
78315
- const installZshCompletion = Effect.fn("cli.completions.install")(function* () {
78316
- if ((yield* HostProcessPlatform) === "win32") return null;
78317
- const fs = yield* FileSystem.FileSystem;
78318
- const path = yield* Path.Path;
78319
- const homeDir = (yield* HostProcessEnvironment).HOME ?? "";
78320
- if (homeDir === "") return null;
78321
- const script = yield* generateZshCompletionScript();
78322
- if (script === null) return null;
78323
- for (const candidate of candidateDirectories({
78324
- homeDir,
78325
- join: path.join
78326
- })) {
78327
- if (candidate.requiresFpathEntry) {
78328
- if (!(yield* fs.makeDirectory(candidate.dir, { recursive: true }).pipe(Effect.as(true), Effect.orElseSucceed(() => false)))) continue;
78329
- } else if (!(yield* fs.exists(candidate.dir).pipe(Effect.orElseSucceed(() => false)))) continue;
78330
- const target = path.join(candidate.dir, COMPLETION_FILE);
78331
- if (!(yield* fs.writeFileString(target, script).pipe(Effect.as(true), Effect.orElseSucceed(() => false)))) continue;
78332
- return {
78333
- path: target,
78334
- requiresFpathEntry: candidate.requiresFpathEntry
78335
- };
78336
- }
78337
- return null;
78338
- });
78339
- function formatZshCompletionInstall(install) {
78340
- const installed = `Installed zsh completions at ${install.path}.`;
78341
- if (!install.requiresFpathEntry) return `${installed} Open a new shell to use them.`;
78342
- return [
78343
- installed,
78344
- `Add this to ~/.zshrc before compinit runs, then open a new shell:`,
78345
- ` fpath=(${install.path.replace(/\/[^/]+$/u, "")} $fpath)`
78346
- ].join("\n");
78347
- }
78348
- //#endregion
78349
78400
  //#region src/cli/service.ts
78350
78401
  const isBootServiceCommandError = Schema$1.is(BootServiceCommandError);
78351
78402
  const bootServiceLayer = (config) => layer$46({
@@ -78400,10 +78451,21 @@ const runServiceCommand = Effect.fn("cli.service.run")(function* (flags, run) {
78400
78451
  if (baseDirNotice !== null) yield* Console.error(baseDirNotice);
78401
78452
  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));
78402
78453
  });
78454
+ /**
78455
+ * Runs on every `service install` and `service update`, including the
78456
+ * already-current paths. Gating it on "the unit changed" meant a host whose
78457
+ * service was already installed — the common case after the first setup — never
78458
+ * got completions at all.
78459
+ */
78460
+ const reportZshCompletionInstall = Effect.gen(function* () {
78461
+ const completion = yield* installZshCompletion().pipe(Effect.orElseSucceed(() => null));
78462
+ if (completion !== null) yield* Console.log(formatZshCompletionInstall(completion));
78463
+ });
78403
78464
  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* () {
78404
78465
  const result = yield* reconcileService();
78405
78466
  if (!result.changed) {
78406
78467
  yield* Console.log(`P4Code service is already installed with @p4code/cli@${version}.`);
78468
+ yield* reportZshCompletionInstall;
78407
78469
  return;
78408
78470
  }
78409
78471
  yield* Console.log(formatReconcileSuccess({
@@ -78412,14 +78474,14 @@ const serviceInstallCommand = Command.make("install", projectLocationFlags).pipe
78412
78474
  cliVersion: version,
78413
78475
  platform: yield* HostProcessPlatform
78414
78476
  }));
78415
- const completion = yield* installZshCompletion().pipe(Effect.orElseSucceed(() => null));
78416
- if (completion !== null) yield* Console.log(formatZshCompletionInstall(completion));
78477
+ yield* reportZshCompletionInstall;
78417
78478
  }))));
78418
78479
  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* () {
78419
78480
  const result = yield* reconcileService();
78420
78481
  if (!result.changed) {
78421
78482
  yield* (yield* BootService).restart;
78422
78483
  yield* Console.log(`Restarted the P4Code service on @p4code/cli@${version}.`);
78484
+ yield* reportZshCompletionInstall;
78423
78485
  return;
78424
78486
  }
78425
78487
  yield* Console.log(formatReconcileSuccess({
@@ -78428,6 +78490,7 @@ const serviceUpdateCommand = Command.make("update", projectLocationFlags).pipe(C
78428
78490
  cliVersion: version,
78429
78491
  platform: yield* HostProcessPlatform
78430
78492
  }));
78493
+ yield* reportZshCompletionInstall;
78431
78494
  }))));
78432
78495
  const serviceUninstallCommand = Command.make("uninstall", projectLocationFlags).pipe(Command.withDescription("Stop and remove the P4Code background service."), Command.withHandler((flags) => runServiceCommand(flags, Effect.gen(function* () {
78433
78496
  const removed = yield* (yield* BootService).uninstall;
@@ -78476,11 +78539,14 @@ const reportUpdateAvailable = Effect.gen(function* () {
78476
78539
  stateDir: paths.stateDir
78477
78540
  });
78478
78541
  if (reminder !== null) yield* Console.error(reminder);
78542
+ const completionsHint = yield* resolveMissingCompletionsHint({ stateDir: paths.stateDir });
78543
+ if (completionsHint !== null) yield* Console.error(completionsHint);
78479
78544
  }).pipe(Effect.ignore);
78480
78545
  const makeCli = () => Command.make("p4", { ...sharedServerCommandFlags }).pipe(Command.withDescription("Run the P4Code server."), Command.withHandler((flags) => runServerCommand(flags)), Command.withSubcommands([
78481
78546
  startCommand,
78482
78547
  serveCommand,
78483
78548
  authCommand,
78549
+ completionsCommand,
78484
78550
  projectCommand,
78485
78551
  serviceCommand
78486
78552
  ]));