@codeworksh/harness 0.0.1-dev.20260922184400 → 0.0.1-dev.20260923102526
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -4
- package/effect.d.mts +221 -93
- package/effect.mjs +2191 -2082
- package/effect.mjs.map +1 -1
- package/package.json +3 -3
package/effect.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
|
19
19
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
20
20
|
import { Bash } from "just-bash";
|
|
21
21
|
import { DirectoryNotFoundError, DirectoryNotFoundError as DirectoryNotFoundError$1, Info, Info as Info$1, NotDirectoryError, NotDirectoryError as NotDirectoryError$1, Ref as Ref$2, Service, Service as Service$8 } from "@codeworksh/plugin/location";
|
|
22
|
+
import { generateModels } from "@codeworksh/aikit/modelgen";
|
|
22
23
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
23
24
|
import path from "node:path";
|
|
24
25
|
import { Script, constants } from "node:vm";
|
|
@@ -102,7 +103,7 @@ var global_exports = /* @__PURE__ */ __exportAll({
|
|
|
102
103
|
authFile: () => authFile,
|
|
103
104
|
defaultLayer: () => defaultLayer$4,
|
|
104
105
|
homeConfig: () => homeConfig,
|
|
105
|
-
layer: () => layer$
|
|
106
|
+
layer: () => layer$27,
|
|
106
107
|
layerWith: () => layerWith$1,
|
|
107
108
|
make: () => make$18,
|
|
108
109
|
resolve: () => resolve$3
|
|
@@ -148,8 +149,8 @@ const build = (input) => Effect.gen(function* () {
|
|
|
148
149
|
]).pipe(Effect.orDie);
|
|
149
150
|
return Service$17.of(paths);
|
|
150
151
|
});
|
|
151
|
-
const layer$
|
|
152
|
-
const defaultLayer$4 = layer$
|
|
152
|
+
const layer$27 = Layer.effect(Service$17, build({}));
|
|
153
|
+
const defaultLayer$4 = layer$27;
|
|
153
154
|
const layerWith$1 = (input) => Layer.effect(Service$17, build(input));
|
|
154
155
|
//#endregion
|
|
155
156
|
//#region src/db/migrations.ts
|
|
@@ -337,7 +338,7 @@ var db_exports = /* @__PURE__ */ __exportAll({
|
|
|
337
338
|
SqlClient: () => SqlClient$1,
|
|
338
339
|
SqlSchema: () => SqlSchema$1,
|
|
339
340
|
defaultLayer: () => defaultLayer$3,
|
|
340
|
-
layer: () => layer$
|
|
341
|
+
layer: () => layer$26,
|
|
341
342
|
locationConfig: () => locationConfig,
|
|
342
343
|
path: () => path$1,
|
|
343
344
|
resolveDatabaseLocation: () => resolveDatabaseLocation
|
|
@@ -351,7 +352,7 @@ const setup = Effect.gen(function* () {
|
|
|
351
352
|
yield* sql`PRAGMA foreign_keys = ON`;
|
|
352
353
|
yield* SqliteMigrator.run({ loader: Migrator.fromRecord(migrations) });
|
|
353
354
|
});
|
|
354
|
-
function layer$
|
|
355
|
+
function layer$26(location) {
|
|
355
356
|
return Layer.unwrap(Effect.gen(function* () {
|
|
356
357
|
if (location !== ":memory:") yield* fileSystem.makeDirectory(posix.dirname(location), { recursive: true }).pipe(Effect.orDie);
|
|
357
358
|
return SqliteClient.layer({
|
|
@@ -370,7 +371,7 @@ const path$1 = Effect.fn("Database.path")(function* (data) {
|
|
|
370
371
|
const directory = data ?? (yield* resolve$3()).data;
|
|
371
372
|
return resolveDatabaseLocation(yield* locationConfig, directory);
|
|
372
373
|
});
|
|
373
|
-
const defaultLayer$3 = Layer.unwrap(path$1().pipe(Effect.map(layer$
|
|
374
|
+
const defaultLayer$3 = Layer.unwrap(path$1().pipe(Effect.map(layer$26)));
|
|
374
375
|
//#endregion
|
|
375
376
|
//#region src/event/schema.ts
|
|
376
377
|
var schema_exports$7 = /* @__PURE__ */ __exportAll({});
|
|
@@ -549,7 +550,7 @@ var SessionEntryPartRow = class extends Model.Class("SessionEntryPartRow")({
|
|
|
549
550
|
/**
|
|
550
551
|
* The public description of a failed execution.
|
|
551
552
|
*
|
|
552
|
-
* Open by design: `type` is a
|
|
553
|
+
* Open by design: `type` is a kebab-case category ending in `-error`, not a closed union.
|
|
553
554
|
* A client older than the server degrades to the message instead of failing to
|
|
554
555
|
* decode, and a new category is an additive change. The typed cause stays
|
|
555
556
|
* server-side -- this is what crosses the wire.
|
|
@@ -562,40 +563,40 @@ const Error$1 = Schema.Struct({
|
|
|
562
563
|
});
|
|
563
564
|
const providerCategory = (tag) => {
|
|
564
565
|
switch (tag) {
|
|
565
|
-
case "Runner.ProviderAuthenticationError": return "provider
|
|
566
|
-
case "Runner.ProviderAuthorizationError": return "provider
|
|
567
|
-
case "Runner.ProviderConfigurationError": return "provider
|
|
568
|
-
case "Runner.ProviderModelUnavailableError": return "provider
|
|
569
|
-
case "Runner.ProviderRateLimitError": return "provider
|
|
570
|
-
case "Runner.ProviderQuotaError": return "provider
|
|
571
|
-
case "Runner.ProviderInvalidRequestError": return "provider
|
|
572
|
-
case "Runner.ProviderContentPolicyError": return "provider
|
|
573
|
-
case "Runner.ProviderTimeoutError": return "provider
|
|
574
|
-
case "Runner.ProviderTransportError": return "provider
|
|
575
|
-
case "Runner.ProviderUnavailableError": return "provider
|
|
576
|
-
case "Runner.ProviderInvalidResponseError": return "provider
|
|
577
|
-
default: return "provider
|
|
566
|
+
case "Runner.ProviderAuthenticationError": return "provider-authentication-error";
|
|
567
|
+
case "Runner.ProviderAuthorizationError": return "provider-authorization-error";
|
|
568
|
+
case "Runner.ProviderConfigurationError": return "provider-configuration-error";
|
|
569
|
+
case "Runner.ProviderModelUnavailableError": return "provider-model-unavailable-error";
|
|
570
|
+
case "Runner.ProviderRateLimitError": return "provider-rate-limit-error";
|
|
571
|
+
case "Runner.ProviderQuotaError": return "provider-quota-error";
|
|
572
|
+
case "Runner.ProviderInvalidRequestError": return "provider-invalid-request-error";
|
|
573
|
+
case "Runner.ProviderContentPolicyError": return "provider-content-filter-error";
|
|
574
|
+
case "Runner.ProviderTimeoutError": return "provider-timeout-error";
|
|
575
|
+
case "Runner.ProviderTransportError": return "provider-transport-error";
|
|
576
|
+
case "Runner.ProviderUnavailableError": return "provider-unavailable-error";
|
|
577
|
+
case "Runner.ProviderInvalidResponseError": return "provider-invalid-response-error";
|
|
578
|
+
default: return "provider-unknown-error";
|
|
578
579
|
}
|
|
579
580
|
};
|
|
580
581
|
const category = (tag) => {
|
|
581
582
|
switch (tag) {
|
|
582
|
-
case "Runner.ModelCatalogError": return "model
|
|
583
|
-
case "Runner.ModelNotFoundError": return "model
|
|
584
|
-
case "Runner.LLMStreamError": return "llm
|
|
585
|
-
case "ContextDecodeError": return "context
|
|
586
|
-
case "ContextEncodeError": return "context
|
|
587
|
-
case "SessionNotFoundError": return "session
|
|
588
|
-
case "SessionLinkedSpaceNotFoundError": return "session
|
|
589
|
-
case "SettingsError": return "settings";
|
|
590
|
-
case "State.SnapshotError": return "state
|
|
591
|
-
case "Runner.SandboxDirectoryNotFoundError": return "sandbox
|
|
592
|
-
case "SandboxFileSystemError": return "sandbox
|
|
583
|
+
case "Runner.ModelCatalogError": return "model-catalog-error";
|
|
584
|
+
case "Runner.ModelNotFoundError": return "model-not-found-error";
|
|
585
|
+
case "Runner.LLMStreamError": return "llm-stream-error";
|
|
586
|
+
case "ContextDecodeError": return "context-decode-error";
|
|
587
|
+
case "ContextEncodeError": return "context-encode-error";
|
|
588
|
+
case "SessionNotFoundError": return "session-not-found-error";
|
|
589
|
+
case "SessionLinkedSpaceNotFoundError": return "session-space-not-found-error";
|
|
590
|
+
case "SettingsError": return "settings-error";
|
|
591
|
+
case "State.SnapshotError": return "state-snapshot-error";
|
|
592
|
+
case "Runner.SandboxDirectoryNotFoundError": return "sandbox-directory-not-found-error";
|
|
593
|
+
case "SandboxFileSystemError": return "sandbox-filesystem-error";
|
|
593
594
|
case "SandboxNotFoundError":
|
|
594
595
|
case "SandboxDriverNotRegisteredError":
|
|
595
596
|
case "SandboxUnavailError":
|
|
596
597
|
case "SandboxRemovedError":
|
|
597
|
-
case "SandboxProviderError": return "sandbox
|
|
598
|
-
default: return "unknown";
|
|
598
|
+
case "SandboxProviderError": return "sandbox-mount-error";
|
|
599
|
+
default: return "unknown-error";
|
|
599
600
|
}
|
|
600
601
|
};
|
|
601
602
|
const message = (cause) => cause instanceof globalThis.Error ? cause.message : typeof cause === "string" ? cause : "Session execution failed";
|
|
@@ -603,7 +604,7 @@ const message = (cause) => cause instanceof globalThis.Error ? cause.message : t
|
|
|
603
604
|
const fromCause = (cause) => {
|
|
604
605
|
const tag = Predicate.hasProperty(cause, "_tag") && typeof cause._tag === "string" ? cause._tag : void 0;
|
|
605
606
|
if (tag === void 0) return {
|
|
606
|
-
type: "unknown",
|
|
607
|
+
type: "unknown-error",
|
|
607
608
|
message: message(cause)
|
|
608
609
|
};
|
|
609
610
|
if (tag === "Runner.ProviderError") {
|
|
@@ -902,7 +903,7 @@ var event_exports = /* @__PURE__ */ __exportAll({
|
|
|
902
903
|
Service: () => Service$16,
|
|
903
904
|
SubscriptionOverflowError: () => SubscriptionOverflowError,
|
|
904
905
|
isSynced: () => isSynced,
|
|
905
|
-
layer: () => layer$
|
|
906
|
+
layer: () => layer$25
|
|
906
907
|
});
|
|
907
908
|
var InvalidDurableEventError = class extends Schema.TaggedError()("InvalidDurableEventError", {
|
|
908
909
|
type: Schema.String,
|
|
@@ -911,7 +912,7 @@ var InvalidDurableEventError = class extends Schema.TaggedError()("InvalidDurabl
|
|
|
911
912
|
const isSynced = (item) => item.type === "log.synced";
|
|
912
913
|
var SubscriptionOverflowError = class extends Schema.TaggedError()("SubscriptionOverflowError", { capacity: Schema.Int }) {};
|
|
913
914
|
var Service$16 = class extends Context.Service()("@codeworksh/harness/event/event/Service") {};
|
|
914
|
-
const layer$
|
|
915
|
+
const layer$25 = Layer.effect(Service$16, Effect.gen(function* () {
|
|
915
916
|
const sql = yield* SqlClient.SqlClient;
|
|
916
917
|
const pubsub = { durable: /* @__PURE__ */ new Map() };
|
|
917
918
|
const subscriptions = /* @__PURE__ */ new Set();
|
|
@@ -1269,7 +1270,7 @@ const archiveEnv = Effect.fn("Space.archiveEnv")(function* (env) {
|
|
|
1269
1270
|
`;
|
|
1270
1271
|
})).pipe(Effect.orDie);
|
|
1271
1272
|
});
|
|
1272
|
-
const layer$
|
|
1273
|
+
const layer$24 = Layer.effect(Service$15, Effect.gen(function* () {
|
|
1273
1274
|
const sql = yield* SqlClient.SqlClient;
|
|
1274
1275
|
const fs = yield* io_exports.SandboxIO.FileSystem;
|
|
1275
1276
|
const { id: env } = yield* io_exports.SandboxIO.Current;
|
|
@@ -1393,7 +1394,7 @@ var session_exports$1 = /* @__PURE__ */ __exportAll({
|
|
|
1393
1394
|
defaultLayer: () => defaultLayer$2,
|
|
1394
1395
|
entryStates: () => entryStates,
|
|
1395
1396
|
entryTypes: () => entryTypes,
|
|
1396
|
-
layer: () => layer$
|
|
1397
|
+
layer: () => layer$23,
|
|
1397
1398
|
messageEntryTypes: () => messageEntryTypes,
|
|
1398
1399
|
partTypes: () => partTypes,
|
|
1399
1400
|
rebaseDirectory: () => rebaseDirectory,
|
|
@@ -1459,7 +1460,7 @@ const decodeMessageEnvelopeIdentity = Schema.decodeUnknownEffect(MessageEnvelope
|
|
|
1459
1460
|
const decodeCompactionData$1 = Schema.decodeUnknownEffect(CompactionData);
|
|
1460
1461
|
const decodeJsonObject$1 = Schema.decodeUnknownEffect(JsonObject);
|
|
1461
1462
|
const encodeJsonObject$1 = Schema.encodeEffect(JsonObject);
|
|
1462
|
-
const layer$
|
|
1463
|
+
const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
|
|
1463
1464
|
const sql = yield* SqlClient.SqlClient;
|
|
1464
1465
|
const events = yield* Service$16;
|
|
1465
1466
|
const findSession = SqlSchema.findOneOption({
|
|
@@ -2121,7 +2122,7 @@ const layer$22 = Layer.effect(Service$14, Effect.gen(function* () {
|
|
|
2121
2122
|
setLabel
|
|
2122
2123
|
});
|
|
2123
2124
|
}));
|
|
2124
|
-
const defaultLayer$2 = layer$
|
|
2125
|
+
const defaultLayer$2 = layer$23.pipe(Layer.provide(defaultLayer$3));
|
|
2125
2126
|
//#endregion
|
|
2126
2127
|
//#region src/context/errors.ts
|
|
2127
2128
|
var ContextDecodeError = class extends Schema.TaggedError()("ContextDecodeError", {
|
|
@@ -2422,7 +2423,7 @@ const assemblePath = Effect.fn("Context.assemblePath")(function* (sessionId, pat
|
|
|
2422
2423
|
//#endregion
|
|
2423
2424
|
//#region src/context/context.ts
|
|
2424
2425
|
var Service$13 = class extends Context.Service()("@codeworksh/harness/context/context/Service") {};
|
|
2425
|
-
const layer$
|
|
2426
|
+
const layer$22 = Layer.effect(Service$13, Effect.gen(function* () {
|
|
2426
2427
|
const sessions = yield* Service$14;
|
|
2427
2428
|
const assemble = Effect.fn("Context.assemble")(function* (sessionId) {
|
|
2428
2429
|
const found = yield* sessions.get(sessionId);
|
|
@@ -2512,7 +2513,7 @@ const make$17 = (vfs) => {
|
|
|
2512
2513
|
};
|
|
2513
2514
|
};
|
|
2514
2515
|
/** The `SandboxFileSystem.Service` backed by the local {@link Vfs}. */
|
|
2515
|
-
const layer$
|
|
2516
|
+
const layer$21 = Layer.effect(filesystem_exports.SandboxFileSystem.Service, Effect.gen(function* () {
|
|
2516
2517
|
return filesystem_exports.SandboxFileSystem.fromProvider(make$17(yield* Vfs));
|
|
2517
2518
|
}));
|
|
2518
2519
|
//#endregion
|
|
@@ -2539,7 +2540,7 @@ var HostProvider = class extends RealFSProvider {
|
|
|
2539
2540
|
* reason to keep relative paths above the mount, not below it — nothing here
|
|
2540
2541
|
* resolves them, and nothing here should.
|
|
2541
2542
|
*/
|
|
2542
|
-
const layer$
|
|
2543
|
+
const layer$20 = () => {
|
|
2543
2544
|
const vfs = create(new HostProvider("/"), {
|
|
2544
2545
|
moduleHooks: false,
|
|
2545
2546
|
virtualCwd: true
|
|
@@ -2632,11 +2633,11 @@ const make$16 = (drivers) => Effect.gen(function* () {
|
|
|
2632
2633
|
encodeRuntimeConfig
|
|
2633
2634
|
});
|
|
2634
2635
|
});
|
|
2635
|
-
const layer$
|
|
2636
|
+
const layer$19 = (...drivers) => Layer.effect(Registry, make$16(drivers));
|
|
2636
2637
|
//#endregion
|
|
2637
2638
|
//#region src/sandbox/shell/host.ts
|
|
2638
2639
|
const DEFAULT_SHELL = "sh";
|
|
2639
|
-
const layer$
|
|
2640
|
+
const layer$18 = (options = {}) => Layer.effect(shell_exports.Shell, Effect.gen(function* () {
|
|
2640
2641
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
2641
2642
|
const run = (label, file, args, opts) => Effect.scoped(Effect.gen(function* () {
|
|
2642
2643
|
const handle = yield* spawner.spawn(ChildProcess.make(file, [...args], {
|
|
@@ -2727,7 +2728,7 @@ const make$15 = Effect.gen(function* () {
|
|
|
2727
2728
|
//#endregion
|
|
2728
2729
|
//#region src/sandbox/control.ts
|
|
2729
2730
|
var Controller = class extends Context.Service()("@codeworksh/harness/sandbox/control/Controller") {};
|
|
2730
|
-
const hostTransport = Layer.provide(Layer.merge(Layer.fresh(layer$
|
|
2731
|
+
const hostTransport = Layer.provide(Layer.merge(Layer.fresh(layer$21), layer$18()), layer$20());
|
|
2731
2732
|
const asDate = DateTime.toDateUtc;
|
|
2732
2733
|
const optionalDate = Option.map(DateTime.toDateUtc);
|
|
2733
2734
|
const MetadataJson = Schema.fromJsonString(Schema.Record(Schema.String, Schema.String));
|
|
@@ -3347,7 +3348,7 @@ const make$14 = Effect.fn("Sandbox.Controller.make")(function* (options) {
|
|
|
3347
3348
|
destroy
|
|
3348
3349
|
});
|
|
3349
3350
|
});
|
|
3350
|
-
const layer$
|
|
3351
|
+
const layer$17 = (options) => Layer.effect(Controller, make$14(options));
|
|
3351
3352
|
//#endregion
|
|
3352
3353
|
//#region src/sandbox/utils/seed.ts
|
|
3353
3354
|
const copyDirectory = async (vfs, sourceDirectory, targetDirectory) => {
|
|
@@ -3603,7 +3604,7 @@ const transport$1 = (inner) => Layer.provideMerge(Layer.effect(shell_exports.She
|
|
|
3603
3604
|
})), inner);
|
|
3604
3605
|
//#endregion
|
|
3605
3606
|
//#region src/sandbox/virtual.ts
|
|
3606
|
-
const transportLayer = (primitives) => Layer.provideMerge(Layer.merge(Layer.fresh(layer$
|
|
3607
|
+
const transportLayer = (primitives) => Layer.provideMerge(Layer.merge(Layer.fresh(layer$21), transport$1(primitives)), primitives);
|
|
3607
3608
|
/** A cwd-neutral FileSystem + just-bash transport over one retained VFS. */
|
|
3608
3609
|
const transport = (vfs) => {
|
|
3609
3610
|
const primitives = Layer.merge(Layer.succeed(Vfs, vfs), unsupported);
|
|
@@ -3710,7 +3711,7 @@ const make$10 = (location, options) => Effect.tryPromise({
|
|
|
3710
3711
|
})
|
|
3711
3712
|
});
|
|
3712
3713
|
const vfsLayer = (location, options) => Layer.effect(Vfs, Effect.acquireRelease(make$10(location, options), ({ provider }) => Effect.sync(() => provider.close())).pipe(Effect.map(({ vfs }) => vfs)));
|
|
3713
|
-
const layer$
|
|
3714
|
+
const layer$16 = (opts) => Layer.merge(vfsLayer(opts?.location, opts?.options), opts?.options?.hostProcess ? host : unsupported);
|
|
3714
3715
|
//#endregion
|
|
3715
3716
|
//#region src/sandbox/drivers/sqldb.ts
|
|
3716
3717
|
const CreateConfig = Schema.Struct({
|
|
@@ -3728,7 +3729,7 @@ const make$9 = () => {
|
|
|
3728
3729
|
const memory = /* @__PURE__ */ new Map();
|
|
3729
3730
|
const runtimeLocation = (input) => input.runtimeConfig.location ?? Option.getOrUndefined(input.providerResourceId);
|
|
3730
3731
|
const persistentTransport = (location) => {
|
|
3731
|
-
const backend = layer$
|
|
3732
|
+
const backend = layer$16({ location });
|
|
3732
3733
|
return transportLayer(Layer.effectContext(Layer.build(backend).pipe(Effect.mapError((cause) => (0, errors_exports.providerError)({
|
|
3733
3734
|
driver: name,
|
|
3734
3735
|
operation: "attach",
|
|
@@ -3863,9 +3864,9 @@ const make$9 = () => {
|
|
|
3863
3864
|
* managers). For a sandbox whose shell should stay inside the VFS, use
|
|
3864
3865
|
* `EnvBash.services` instead, which wires just-bash over the same tree.
|
|
3865
3866
|
*/
|
|
3866
|
-
const services = (backend, identity) => Layer.provideMerge(io_exports.SandboxIO.mount(identity), Layer.provideMerge(Layer.merge(layer$
|
|
3867
|
+
const services = (backend, identity) => Layer.provideMerge(io_exports.SandboxIO.mount(identity), Layer.provideMerge(Layer.merge(layer$21, layer$18()), backend));
|
|
3867
3868
|
/** Default sandbox: the real OS filesystem and processes, mounted at `cwd`. */
|
|
3868
|
-
const defaultLayer$1 = (cwd) => services(layer$
|
|
3869
|
+
const defaultLayer$1 = (cwd) => services(layer$20(), io_exports.SandboxIO.host(cwd));
|
|
3869
3870
|
//#endregion
|
|
3870
3871
|
//#region src/git/git.ts
|
|
3871
3872
|
var AppProcessError = class extends Schema.TaggedError()("AppProcessError", {
|
|
@@ -3875,7 +3876,7 @@ var AppProcessError = class extends Schema.TaggedError()("AppProcessError", {
|
|
|
3875
3876
|
cause: Schema.optional(Schema.Defect())
|
|
3876
3877
|
}) {};
|
|
3877
3878
|
var Service$12 = class extends Context.Service()("@codeworksh/harness/git/git/Service") {};
|
|
3878
|
-
const layer$
|
|
3879
|
+
const layer$15 = Layer.effect(Service$12, Effect.gen(function* () {
|
|
3879
3880
|
const shell = yield* io_exports.SandboxIO.Shell;
|
|
3880
3881
|
const execute = (cwd) => (args, options) => shell.execArgv(["git", ...args], {
|
|
3881
3882
|
cwd,
|
|
@@ -4099,7 +4100,7 @@ function parts(host, name) {
|
|
|
4099
4100
|
if (!host || !pathname) return void 0;
|
|
4100
4101
|
return `${host}/${pathname}`.toLowerCase();
|
|
4101
4102
|
}
|
|
4102
|
-
const layer$
|
|
4103
|
+
const layer$14 = Layer.effect(Service$11, Effect.gen(function* () {
|
|
4103
4104
|
const fs = yield* io_exports.SandboxIO.FileSystem;
|
|
4104
4105
|
const git = yield* Service$12;
|
|
4105
4106
|
const find = Effect.fn("Repo.find")(function* (input) {
|
|
@@ -4156,7 +4157,7 @@ var WorktreeError = class extends Schema.TaggedError()("Worktree.WorktreeError",
|
|
|
4156
4157
|
cause: Schema.optional(Schema.Defect())
|
|
4157
4158
|
}) {};
|
|
4158
4159
|
var Service$10 = class extends Context.Service()("@codeworksh/harness/worktree/worktree/Service") {};
|
|
4159
|
-
const layer$
|
|
4160
|
+
const layer$13 = Layer.effect(Service$10, Effect.gen(function* () {
|
|
4160
4161
|
const fs = yield* io_exports.SandboxIO.FileSystem;
|
|
4161
4162
|
const git = yield* Service$12;
|
|
4162
4163
|
const worktree = Effect.fnUntraced(function* (operation, cwd, args, directory) {
|
|
@@ -4211,7 +4212,7 @@ const provisional = (env, location) => schema_exports$6.ProjectSchema.ID.make(fa
|
|
|
4211
4212
|
location
|
|
4212
4213
|
])));
|
|
4213
4214
|
var Service$9 = class extends Context.Service()("@codeworksh/harness/project/project/Service") {};
|
|
4214
|
-
const layer$
|
|
4215
|
+
const layer$12 = Layer.effect(Service$9, Effect.gen(function* () {
|
|
4215
4216
|
const sql = yield* SqlClient.SqlClient;
|
|
4216
4217
|
const fs = yield* io_exports.SandboxIO.FileSystem;
|
|
4217
4218
|
const repos = yield* Service$11;
|
|
@@ -4353,11 +4354,11 @@ var location_exports = /* @__PURE__ */ __exportAll({
|
|
|
4353
4354
|
Ref: () => Ref$2,
|
|
4354
4355
|
Service: () => Service$8,
|
|
4355
4356
|
defaultLayer: () => defaultLayer,
|
|
4356
|
-
layer: () => layer$
|
|
4357
|
+
layer: () => layer$11,
|
|
4357
4358
|
layerMounted: () => layerMounted,
|
|
4358
4359
|
layerWith: () => layerWith
|
|
4359
4360
|
});
|
|
4360
|
-
const layer$
|
|
4361
|
+
const layer$11 = (ref = {}) => Layer.effect(Service, Effect.gen(function* () {
|
|
4361
4362
|
const instance = yield* io_exports.SandboxIO.Current;
|
|
4362
4363
|
const directory = schema_exports$9.AbsolutePath.make(ref.directory ?? instance.cwd);
|
|
4363
4364
|
const resolved = yield* (yield* Service$9).resolveOrCreate(directory);
|
|
@@ -4372,7 +4373,7 @@ const layer$10 = (ref = {}) => Layer.effect(Service, Effect.gen(function* () {
|
|
|
4372
4373
|
* folded in. What remains is exactly the mount and the database — for callers
|
|
4373
4374
|
* that already hold both, such as the runner and `Session.create`.
|
|
4374
4375
|
*/
|
|
4375
|
-
const layerMounted = (ref = {}) => layer$
|
|
4376
|
+
const layerMounted = (ref = {}) => layer$11(ref).pipe(Layer.provide(layer$12.pipe(Layer.provide(Layer.mergeAll(layer$14, layer$13, layer$24)), Layer.provide(layer$15))));
|
|
4376
4377
|
const layerWith = (ref, sandbox) => layerMounted(ref).pipe(Layer.provide(sandbox), Layer.provide(defaultLayer$3));
|
|
4377
4378
|
const defaultLayer = (ref, path) => layerWith(ref, defaultLayer$1(path));
|
|
4378
4379
|
//#endregion
|
|
@@ -4725,7 +4726,7 @@ var control_exports = /* @__PURE__ */ __exportAll({
|
|
|
4725
4726
|
Control: () => control_exports,
|
|
4726
4727
|
PromptConflictError: () => PromptConflictError,
|
|
4727
4728
|
Service: () => Service$5,
|
|
4728
|
-
layer: () => layer$
|
|
4729
|
+
layer: () => layer$10
|
|
4729
4730
|
});
|
|
4730
4731
|
/**
|
|
4731
4732
|
* The prompt could not be admitted under the id it was given: either that id
|
|
@@ -4737,7 +4738,7 @@ var PromptConflictError = class extends Schema.TaggedError()("PromptConflictErro
|
|
|
4737
4738
|
messageId: Schema.String
|
|
4738
4739
|
}) {};
|
|
4739
4740
|
var Service$5 = class extends Context.Service()("@codeworksh/harness/control/Service") {};
|
|
4740
|
-
const layer$
|
|
4741
|
+
const layer$10 = Layer.effect(Service$5, Effect.gen(function* () {
|
|
4741
4742
|
const sessions = yield* Service$14;
|
|
4742
4743
|
const inputs = yield* make$8;
|
|
4743
4744
|
const execution = yield* Service$6;
|
|
@@ -4806,7 +4807,7 @@ var registry_exports = /* @__PURE__ */ __exportAll({
|
|
|
4806
4807
|
EventRegistry: () => registry_exports,
|
|
4807
4808
|
Service: () => Service$4,
|
|
4808
4809
|
flatten: () => flatten,
|
|
4809
|
-
layer: () => layer$
|
|
4810
|
+
layer: () => layer$9
|
|
4810
4811
|
});
|
|
4811
4812
|
var Service$4 = class extends Context.Service()("@codeworksh/harness/event/registry/Service") {};
|
|
4812
4813
|
var EventRegistrationError = class extends Schema.TaggedError()("EventRegistrationError", {
|
|
@@ -4852,7 +4853,7 @@ const flatten = Effect.fn("EventRegistry.flatten")(function* (plugins) {
|
|
|
4852
4853
|
}
|
|
4853
4854
|
return [...Definitions, ...definitions];
|
|
4854
4855
|
});
|
|
4855
|
-
const layer$
|
|
4856
|
+
const layer$9 = (definitions = Definitions) => Layer.sync(Service$4, () => {
|
|
4856
4857
|
let current = definitions;
|
|
4857
4858
|
let latest = schema_exports$7.EventSchema.latest(current);
|
|
4858
4859
|
const replace = Effect.fn("EventRegistry.replace")(function* (plugins) {
|
|
@@ -4869,2100 +4870,2223 @@ const layer$8 = (definitions = Definitions) => Layer.sync(Service$4, () => {
|
|
|
4869
4870
|
});
|
|
4870
4871
|
});
|
|
4871
4872
|
//#endregion
|
|
4872
|
-
//#region src/
|
|
4873
|
+
//#region src/model/catalog.ts
|
|
4874
|
+
var catalog_exports = /* @__PURE__ */ __exportAll({
|
|
4875
|
+
ModelCatalog: () => catalog_exports,
|
|
4876
|
+
RefreshError: () => RefreshError,
|
|
4877
|
+
filename: () => filename,
|
|
4878
|
+
layer: () => layer$8,
|
|
4879
|
+
loadError: () => loadError,
|
|
4880
|
+
models: () => models,
|
|
4881
|
+
providers: () => providers,
|
|
4882
|
+
refresh: () => refresh$1,
|
|
4883
|
+
sync: () => sync,
|
|
4884
|
+
ttlConfig: () => ttlConfig
|
|
4885
|
+
});
|
|
4886
|
+
const filename = "models.gen.json";
|
|
4887
|
+
/** How old the catalog may get before a check downloads a new one. */
|
|
4888
|
+
const ttlConfig = Config.Duration("CODEWORK_MODELS_TTL").pipe(Config.withDefault(Duration.minutes(15)));
|
|
4889
|
+
/** How often a server looks at the file. A look is a `stat`; only a stale file costs a download. */
|
|
4890
|
+
const checkEvery = Duration.minutes(1);
|
|
4891
|
+
var RefreshError = class extends Schema.TaggedError()("ModelCatalog.RefreshError", {
|
|
4892
|
+
path: Schema.String,
|
|
4893
|
+
detail: Schema.String
|
|
4894
|
+
}) {
|
|
4895
|
+
get message() {
|
|
4896
|
+
return `failed to refresh the model catalog at ${this.path}: ${this.detail}`;
|
|
4897
|
+
}
|
|
4898
|
+
};
|
|
4899
|
+
/** Point aikit at `<home>/models.gen.json`, which `CODEWORK_MODELS_FILE` overrides. */
|
|
4900
|
+
const use = (home) => {
|
|
4901
|
+
const own = hostPath.resolve(home, filename);
|
|
4902
|
+
Model$1.configureCatalog(own);
|
|
4903
|
+
const path = Model$1.catalogPath();
|
|
4904
|
+
return {
|
|
4905
|
+
path,
|
|
4906
|
+
pinned: path !== own
|
|
4907
|
+
};
|
|
4908
|
+
};
|
|
4909
|
+
const modified = (path) => fileSystem.stat(path).pipe(Effect.map((info) => Option.map(info.mtime, (date) => date.getTime())), Effect.orElseSucceed(() => Option.none()));
|
|
4910
|
+
const stale = Effect.fn("ModelCatalog.stale")(function* (path) {
|
|
4911
|
+
const mtime = yield* modified(path);
|
|
4912
|
+
if (Option.isNone(mtime)) return true;
|
|
4913
|
+
const ttl = yield* ttlConfig;
|
|
4914
|
+
return (yield* Clock.currentTimeMillis) - mtime.value >= Duration.toMillis(ttl);
|
|
4915
|
+
});
|
|
4916
|
+
const download$1 = (path) => Effect.tryPromise({
|
|
4917
|
+
try: () => generateModels({ path }),
|
|
4918
|
+
catch: (cause) => new RefreshError({
|
|
4919
|
+
path,
|
|
4920
|
+
detail: cause instanceof Error ? cause.message : String(cause)
|
|
4921
|
+
})
|
|
4922
|
+
});
|
|
4923
|
+
/** Download a new catalog for the home when it is missing, older than the TTL, or `force` asks. */
|
|
4924
|
+
const refresh$1 = Effect.fn("ModelCatalog.refresh")(function* (home, options = {}) {
|
|
4925
|
+
const { path } = use(home);
|
|
4926
|
+
if (options.force !== true && !(yield* stale(path))) return {
|
|
4927
|
+
path,
|
|
4928
|
+
refreshed: false
|
|
4929
|
+
};
|
|
4930
|
+
yield* download$1(path);
|
|
4931
|
+
return {
|
|
4932
|
+
path,
|
|
4933
|
+
refreshed: true
|
|
4934
|
+
};
|
|
4935
|
+
});
|
|
4873
4936
|
/**
|
|
4874
|
-
*
|
|
4875
|
-
*
|
|
4876
|
-
*
|
|
4937
|
+
* The automatic check: refresh a stale catalog unless it is pinned, and never fail. A failed
|
|
4938
|
+
* download leaves the old file in place; a missing one surfaces later as a
|
|
4939
|
+
* `Runner.ModelCatalogError` from whatever needed it.
|
|
4877
4940
|
*/
|
|
4878
|
-
const
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4941
|
+
const check$1 = Effect.fn("ModelCatalog.check")(function* ({ path, pinned }) {
|
|
4942
|
+
if (pinned || !(yield* stale(path))) return;
|
|
4943
|
+
yield* download$1(path);
|
|
4944
|
+
});
|
|
4945
|
+
const quietly = (effect) => effect.pipe(Effect.asVoid, Effect.catch((error) => Effect.logWarning(`model catalog: ${error.message}`)));
|
|
4946
|
+
/** Check the home's catalog once, for a command that runs and exits; returns the file aikit reads. */
|
|
4947
|
+
const sync = Effect.fn("ModelCatalog.sync")(function* (home) {
|
|
4948
|
+
const target = use(home);
|
|
4949
|
+
yield* quietly(check$1(target));
|
|
4950
|
+
return target.path;
|
|
4951
|
+
});
|
|
4952
|
+
/**
|
|
4953
|
+
* Check at boot, and with `watch`, keep checking for the life of the layer. A file whose mtime
|
|
4954
|
+
* moved -- refreshed here, by `codework models refresh`, or by hand -- is reloaded, so the
|
|
4955
|
+
* process never serves a catalog older than what is on disk.
|
|
4956
|
+
*/
|
|
4957
|
+
const layer$8 = (options) => Layer.effectDiscard(Effect.gen(function* () {
|
|
4958
|
+
const target = use(options.home);
|
|
4959
|
+
yield* quietly(check$1(target));
|
|
4960
|
+
if (!options.watch) return;
|
|
4961
|
+
const current = modified(target.path).pipe(Effect.map(Option.getOrUndefined));
|
|
4962
|
+
const seen = yield* Ref.make(yield* current);
|
|
4963
|
+
const tick = Effect.gen(function* () {
|
|
4964
|
+
yield* quietly(check$1(target));
|
|
4965
|
+
const mtime = yield* current;
|
|
4966
|
+
if (mtime !== (yield* Ref.getAndSet(seen, mtime))) Model$1.reloadCatalog();
|
|
4967
|
+
});
|
|
4968
|
+
yield* Effect.forkScoped(tick.pipe(Effect.repeat(Schedule.spaced(checkEvery))));
|
|
4969
|
+
}));
|
|
4970
|
+
/** Translate aikit's catalog failure, or undefined for anything else. */
|
|
4971
|
+
const loadError = (cause) => {
|
|
4972
|
+
if (typeof cause !== "object" || cause === null || !("name" in cause) || cause.name !== "ModelCatalogLoadError") return;
|
|
4973
|
+
if (!("data" in cause) || typeof cause.data !== "object" || cause.data === null) return void 0;
|
|
4974
|
+
const data = cause.data;
|
|
4975
|
+
if (!("path" in data) || typeof data.path !== "string" || !("message" in data) || typeof data.message !== "string" || !("reason" in data) || data.reason !== "missing" && data.reason !== "unreadable" && data.reason !== "empty" && data.reason !== "invalid") return;
|
|
4976
|
+
return new ModelCatalogError({
|
|
4977
|
+
path: data.path,
|
|
4978
|
+
reason: data.reason,
|
|
4979
|
+
detail: data.message
|
|
4980
|
+
});
|
|
4882
4981
|
};
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
}
|
|
4896
|
-
/** Normalize nullable properties before schema validation, without changing array positions. */
|
|
4897
|
-
const normalize$1 = copy;
|
|
4982
|
+
const catalogError = (cause) => loadError(cause) ?? new ModelCatalogError({
|
|
4983
|
+
path: Model$1.catalogPath(),
|
|
4984
|
+
reason: "unreadable",
|
|
4985
|
+
detail: cause instanceof Error ? cause.message : "failed to load the model catalog"
|
|
4986
|
+
});
|
|
4987
|
+
const models = Effect.tryPromise({
|
|
4988
|
+
try: () => Model$1.getBuiltInModels(),
|
|
4989
|
+
catch: catalogError
|
|
4990
|
+
});
|
|
4991
|
+
const providers = Effect.tryPromise({
|
|
4992
|
+
try: () => Model$1.getProviders(),
|
|
4993
|
+
catch: catalogError
|
|
4994
|
+
});
|
|
4898
4995
|
//#endregion
|
|
4899
|
-
//#region src/
|
|
4900
|
-
|
|
4901
|
-
*
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4908
|
-
* of this software and associated documentation files (the "Software"), to deal
|
|
4909
|
-
* in the Software without restriction, including without limitation the rights
|
|
4910
|
-
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
4911
|
-
* copies of the Software, and to permit persons to whom the Software is
|
|
4912
|
-
* furnished to do so, subject to the following conditions:
|
|
4913
|
-
*
|
|
4914
|
-
* The above copyright notice and this permission notice shall be included in all
|
|
4915
|
-
* copies or substantial portions of the Software.
|
|
4916
|
-
*
|
|
4917
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
4918
|
-
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
4919
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
4920
|
-
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
4921
|
-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
4922
|
-
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
4923
|
-
* SOFTWARE.
|
|
4924
|
-
*/
|
|
4996
|
+
//#region src/plugin/plugin.ts
|
|
4997
|
+
var plugin_exports$1 = /* @__PURE__ */ __exportAll({});
|
|
4998
|
+
import * as import__codeworksh_plugin_plugin from "@codeworksh/plugin/plugin";
|
|
4999
|
+
__reExport(plugin_exports$1, import__codeworksh_plugin_plugin);
|
|
5000
|
+
//#endregion
|
|
5001
|
+
//#region src/plugin/builtin/prompt/default.ts
|
|
5002
|
+
/** The default coding-agent foundation, used unless `promptCustom` replaces it. */
|
|
5003
|
+
const foundation = `You are an expert coding assistant operating inside codework, a coding agent harness.`;
|
|
4925
5004
|
/**
|
|
4926
|
-
*
|
|
4927
|
-
*
|
|
4928
|
-
* A plugin is installed into the store as its own npm tree, so `effect` and the plugin SDK land
|
|
4929
|
-
* there a second time. Two copies of the *same version* are still two module instances, and
|
|
4930
|
-
* Effect Schema's checks do not survive the crossing: a schema built by one copy and evaluated by
|
|
4931
|
-
* the other reports every check as failed, so a tool whose success type is `Schema.Finite` dies
|
|
4932
|
-
* on "Expected a finite number" for `22800`. Nothing about that is visible at registration --
|
|
4933
|
-
* `Schema.isSchema` passes, the JSON Schema is derived correctly, and the tool reaches the model
|
|
4934
|
-
* -- so it surfaces on the first call rather than at boot.
|
|
5005
|
+
* Guidelines that hold regardless of which tools are registered.
|
|
4935
5006
|
*
|
|
4936
|
-
*
|
|
4937
|
-
*
|
|
4938
|
-
* whichever one is running now, and two applications embedding the harness need not agree on
|
|
4939
|
-
* where their `effect` lives.
|
|
5007
|
+
* Kept short on purpose. Every line here is spent on every request, so a line
|
|
5008
|
+
* earns its place only if a model measurably behaves worse without it.
|
|
4940
5009
|
*/
|
|
4941
|
-
const
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
5010
|
+
const standingGuidelines = [
|
|
5011
|
+
"Be concise. Report what you did and what you found, not what you are about to do.",
|
|
5012
|
+
"Quote exact paths and command output rather than paraphrasing them.",
|
|
5013
|
+
"If a command fails, read the error before retrying."
|
|
5014
|
+
];
|
|
5015
|
+
/** Collapse whitespace so two spellings of one guideline dedupe against each other. */
|
|
5016
|
+
const normalize$1 = (value) => value.trim().replace(/\s+/g, " ");
|
|
4948
5017
|
/**
|
|
4949
|
-
*
|
|
4950
|
-
*
|
|
5018
|
+
* Tool-contributed guidelines followed by the standing ones, normalized and
|
|
5019
|
+
* deduplicated with first occurrence winning.
|
|
5020
|
+
*
|
|
5021
|
+
* Tool order is bucket order, which is the same order the index renders in and the
|
|
5022
|
+
* same order the provider receives definitions in -- one ordering, not three.
|
|
4951
5023
|
*/
|
|
4952
|
-
|
|
4953
|
-
const
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
}
|
|
4961
|
-
/** Use Node's main loader even when the caller runs inside a bundler or test VM. */
|
|
4962
|
-
async function importModule(specifier) {
|
|
4963
|
-
const imported = await withSharedModules(() => new Script(`import(${JSON.stringify(specifier)})`, { importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER }).runInThisContext());
|
|
4964
|
-
if (typeof imported !== "object" || imported === null) return imported;
|
|
4965
|
-
const module = imported;
|
|
4966
|
-
const exports = module["module.exports"];
|
|
4967
|
-
if (exports !== module.default || typeof exports !== "object" && typeof exports !== "function" || exports === null) return imported;
|
|
4968
|
-
return Object.assign({}, module, exports);
|
|
4969
|
-
}
|
|
4970
|
-
/** Node resolution scoped synchronously to the caller. */
|
|
4971
|
-
function resolveModule(specifier, directory, conditions) {
|
|
4972
|
-
const hook = registerHooks({ resolve(target, context, nextResolve) {
|
|
4973
|
-
return nextResolve(target, {
|
|
4974
|
-
...context,
|
|
4975
|
-
parentURL: pathToFileURL(path.join(directory, "package.json")).href,
|
|
4976
|
-
...conditions === void 0 ? {} : { conditions: [.../* @__PURE__ */ new Set([...context.conditions, ...conditions])] }
|
|
4977
|
-
});
|
|
4978
|
-
} });
|
|
4979
|
-
try {
|
|
4980
|
-
const resolve = (target) => {
|
|
4981
|
-
const resolved = import.meta.resolve(path.isAbsolute(target) ? pathToFileURL(target).href : target);
|
|
4982
|
-
if (resolved.startsWith("file:")) statSync(new URL(resolved));
|
|
4983
|
-
return resolved;
|
|
4984
|
-
};
|
|
4985
|
-
try {
|
|
4986
|
-
return resolve(specifier);
|
|
4987
|
-
} catch (error) {
|
|
4988
|
-
if (path.extname(specifier) || !missing(error)) throw error;
|
|
4989
|
-
for (const extension of [
|
|
4990
|
-
".ts",
|
|
4991
|
-
".tsx",
|
|
4992
|
-
".js",
|
|
4993
|
-
".jsx",
|
|
4994
|
-
".mts",
|
|
4995
|
-
".mjs",
|
|
4996
|
-
".cts",
|
|
4997
|
-
".cjs"
|
|
4998
|
-
]) try {
|
|
4999
|
-
return resolve(specifier + extension);
|
|
5000
|
-
} catch (cause) {
|
|
5001
|
-
if (!missing(cause)) throw cause;
|
|
5002
|
-
}
|
|
5003
|
-
throw error;
|
|
5004
|
-
}
|
|
5005
|
-
} finally {
|
|
5006
|
-
hook.deregister();
|
|
5024
|
+
const guidelines = (tools) => {
|
|
5025
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5026
|
+
const collected = [];
|
|
5027
|
+
for (const line of [...tools.flatMap((tool) => tool.promptGuidelines ?? []), ...standingGuidelines]) {
|
|
5028
|
+
const normalized = normalize$1(line);
|
|
5029
|
+
if (normalized.length === 0 || seen.has(normalized)) continue;
|
|
5030
|
+
seen.add(normalized);
|
|
5031
|
+
collected.push(normalized);
|
|
5007
5032
|
}
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
return error instanceof Error && "code" in error && [
|
|
5011
|
-
"ENOENT",
|
|
5012
|
-
"ENOTDIR",
|
|
5013
|
-
"ERR_MODULE_NOT_FOUND"
|
|
5014
|
-
].includes(String(error.code));
|
|
5015
|
-
}
|
|
5016
|
-
//#endregion
|
|
5017
|
-
//#region src/plugin/error.ts
|
|
5033
|
+
return collected;
|
|
5034
|
+
};
|
|
5018
5035
|
/**
|
|
5019
|
-
* The
|
|
5020
|
-
*
|
|
5036
|
+
* The rendered tool index.
|
|
5037
|
+
*
|
|
5038
|
+
* `(none)` rather than an omitted section: a model told it has no tools behaves
|
|
5039
|
+
* better than one left to infer it from silence.
|
|
5021
5040
|
*/
|
|
5022
|
-
const
|
|
5041
|
+
const toolIndex = (tools) => {
|
|
5042
|
+
const listed = tools.filter((tool) => tool.promptSnippet !== void 0 && tool.promptSnippet.length > 0);
|
|
5043
|
+
if (listed.length === 0) return "(none)";
|
|
5044
|
+
return listed.map((tool) => `- ${tool.name}: ${tool.promptSnippet}`).join("\n");
|
|
5045
|
+
};
|
|
5046
|
+
const assemble = (input) => {
|
|
5047
|
+
const sections = [input.promptCustom ?? foundation];
|
|
5048
|
+
sections.push(`Available tools:\n${toolIndex(input.tools)}`);
|
|
5049
|
+
const lines = guidelines(input.tools);
|
|
5050
|
+
if (lines.length > 0) sections.push(`Guidelines:\n${lines.map((line) => `- ${line}`).join("\n")}`);
|
|
5051
|
+
const append = input.promptSystemAppend?.trim();
|
|
5052
|
+
if (append !== void 0 && append.length > 0) sections.push(append);
|
|
5053
|
+
sections.push(`Current working directory: ${input.directory}`);
|
|
5054
|
+
return sections.join("\n\n");
|
|
5055
|
+
};
|
|
5023
5056
|
/**
|
|
5024
|
-
*
|
|
5025
|
-
*
|
|
5026
|
-
* Absent for an embedder's own list and for anything the CLI was handed directly -- in both cases
|
|
5027
|
-
* the caller already knows where the string came from. It is the settings path that needs saying,
|
|
5028
|
-
* because several layers accumulate entries and "which file says this" is the question.
|
|
5057
|
+
* A caller slot, awaited lazily. A throw or rejection fails the snapshot, which the
|
|
5058
|
+
* host attributes to this plugin's id.
|
|
5029
5059
|
*/
|
|
5030
|
-
const
|
|
5060
|
+
const slot = (ctx, resolver) => Effect.tryPromise(() => Promise.resolve(resolver?.(ctx)));
|
|
5061
|
+
const defaultPromptPlugin = (0, plugin_exports$1.define)({
|
|
5062
|
+
id: "codework.prompt.default",
|
|
5063
|
+
kind: "prompt",
|
|
5064
|
+
setup: Effect.fn("DefaultPromptPlugin.setup")(function* (ctx) {
|
|
5065
|
+
const custom = yield* slot(ctx, ctx.config.promptCustom);
|
|
5066
|
+
const append = yield* slot(ctx, ctx.config.promptSystemAppend);
|
|
5067
|
+
ctx.plugin.prompt.set(assemble({
|
|
5068
|
+
tools: ctx.plugin.tools.list(),
|
|
5069
|
+
directory: ctx.location.directory,
|
|
5070
|
+
...custom === void 0 ? {} : { promptCustom: custom },
|
|
5071
|
+
...append === void 0 ? {} : { promptSystemAppend: append }
|
|
5072
|
+
}));
|
|
5073
|
+
})
|
|
5074
|
+
});
|
|
5075
|
+
//#endregion
|
|
5076
|
+
//#region src/tool/truncate.ts
|
|
5031
5077
|
/**
|
|
5032
|
-
*
|
|
5078
|
+
* Shared truncation utilities for tool outputs.
|
|
5033
5079
|
*
|
|
5034
|
-
*
|
|
5035
|
-
*
|
|
5036
|
-
*
|
|
5037
|
-
*/
|
|
5038
|
-
var SourceError = class extends Schema.TaggedError()("PluginSourceError", {
|
|
5039
|
-
reason: Schema.Literals([
|
|
5040
|
-
"plugin-unsupported-source",
|
|
5041
|
-
"plugin-not-found",
|
|
5042
|
-
"plugin-escapes-root"
|
|
5043
|
-
]),
|
|
5044
|
-
reference: Reference,
|
|
5045
|
-
file: File$1,
|
|
5046
|
-
message: Schema.String
|
|
5047
|
-
}) {};
|
|
5048
|
-
/** A spec that is installable, and whose install did not produce a usable package. */
|
|
5049
|
-
var InstallError = class extends Schema.TaggedError()("PluginInstallError", {
|
|
5050
|
-
reason: Schema.Literals([
|
|
5051
|
-
"plugin-resolve-failed",
|
|
5052
|
-
"plugin-fetch-failed",
|
|
5053
|
-
"plugin-no-commit",
|
|
5054
|
-
"plugin-no-entrypoint"
|
|
5055
|
-
]),
|
|
5056
|
-
reference: Reference,
|
|
5057
|
-
file: File$1,
|
|
5058
|
-
message: Schema.String,
|
|
5059
|
-
cause: Schema.optional(Schema.Defect())
|
|
5060
|
-
}) {};
|
|
5061
|
-
/**
|
|
5062
|
-
* A module that reached the import and did not come back as a plugin.
|
|
5063
|
-
*
|
|
5064
|
-
* Distinct from {@link InstallError}: the bytes are here and are fine as *bytes*. What failed is
|
|
5065
|
-
* evaluating them, or what they turned out to export -- which is the distinction a caller acts on,
|
|
5066
|
-
* because one is fixed by installing and the other by editing code.
|
|
5067
|
-
*/
|
|
5068
|
-
var LoadError = class extends Schema.TaggedError()("PluginLoadError", {
|
|
5069
|
-
reason: Schema.Literals([
|
|
5070
|
-
"plugin-import-failed",
|
|
5071
|
-
"plugin-missing-dependency",
|
|
5072
|
-
"plugin-not-compiled",
|
|
5073
|
-
"plugin-invalid-definition"
|
|
5074
|
-
]),
|
|
5075
|
-
reference: Reference,
|
|
5076
|
-
file: File$1,
|
|
5077
|
-
message: Schema.String,
|
|
5078
|
-
/** The ID the module declared, when it got far enough to declare one. */
|
|
5079
|
-
id: Schema.optional(Schema.String),
|
|
5080
|
-
cause: Schema.optional(Schema.Defect())
|
|
5081
|
-
}) {};
|
|
5082
|
-
/** The store's own failures: what is filed, what is not, and what could not be claimed. */
|
|
5083
|
-
var StoreError = class extends Schema.TaggedError()("PluginStoreError", {
|
|
5084
|
-
reason: Schema.Literals([
|
|
5085
|
-
"plugin-not-installed",
|
|
5086
|
-
"plugin-lock-timeout",
|
|
5087
|
-
"plugin-marker-invalid",
|
|
5088
|
-
"plugin-index-invalid",
|
|
5089
|
-
"plugin-collect-failed"
|
|
5090
|
-
]),
|
|
5091
|
-
reference: Reference,
|
|
5092
|
-
file: File$1,
|
|
5093
|
-
message: Schema.String,
|
|
5094
|
-
cause: Schema.optional(Schema.Defect())
|
|
5095
|
-
}) {};
|
|
5096
|
-
/**
|
|
5097
|
-
* Note which settings file declared the entry a failure came from.
|
|
5098
|
-
*
|
|
5099
|
-
* The store and the installer raise their own failures without knowing that, because neither is
|
|
5100
|
-
* told: they are handed a spec, not a settings layer. The loader knows, and this is where it says
|
|
5101
|
-
* so -- leaving an existing answer alone, since a more specific one is already better.
|
|
5102
|
-
*/
|
|
5103
|
-
const declaredIn = (error, file) => {
|
|
5104
|
-
if (error.file !== void 0) return error;
|
|
5105
|
-
const shared = {
|
|
5106
|
-
reference: error.reference,
|
|
5107
|
-
message: error.message,
|
|
5108
|
-
file
|
|
5109
|
-
};
|
|
5110
|
-
if (Schema.is(SourceError)(error)) return new SourceError({
|
|
5111
|
-
...shared,
|
|
5112
|
-
reason: error.reason
|
|
5113
|
-
});
|
|
5114
|
-
if (Schema.is(InstallError)(error)) return new InstallError({
|
|
5115
|
-
...shared,
|
|
5116
|
-
reason: error.reason,
|
|
5117
|
-
cause: error.cause
|
|
5118
|
-
});
|
|
5119
|
-
if (Schema.is(LoadError)(error)) return new LoadError({
|
|
5120
|
-
...shared,
|
|
5121
|
-
reason: error.reason,
|
|
5122
|
-
id: error.id,
|
|
5123
|
-
cause: error.cause
|
|
5124
|
-
});
|
|
5125
|
-
if (Schema.is(StoreError)(error)) return new StoreError({
|
|
5126
|
-
...shared,
|
|
5127
|
-
reason: error.reason,
|
|
5128
|
-
cause: error.cause
|
|
5129
|
-
});
|
|
5130
|
-
return error;
|
|
5131
|
-
};
|
|
5132
|
-
//#endregion
|
|
5133
|
-
//#region src/util/path.ts
|
|
5134
|
-
/**
|
|
5135
|
-
* A directory that crosses into the harness is absolute, and a relative one is a **defect** rather
|
|
5136
|
-
* than an error.
|
|
5137
|
-
*
|
|
5138
|
-
* A relative path is a disguised `process.cwd()` call: and every consumer
|
|
5139
|
-
* resolves it silently. `path.resolve("x")` anchors to the process directory, npm answers a
|
|
5140
|
-
* relative `cwd` with a `localPrefix` of whatever repository the process is inside (measured, not
|
|
5141
|
-
* assumed), and `Session.link` would write the *server's* directory into a field whose whole
|
|
5142
|
-
* purpose is to be a host path the client declared.
|
|
5143
|
-
*
|
|
5144
|
-
* For a long-running server the process directory is nobody's project, so the result is a
|
|
5145
|
-
* plausible answer to a question no one asked.
|
|
5146
|
-
* An absolute path is the one spelling none of them can quietly reinterpret.
|
|
5147
|
-
*
|
|
5148
|
-
* A defect rather than a typed error because nothing a user types reaches here. Every reference
|
|
5149
|
-
* goes through `parse`, and every directory comes from `Global.resolve`, a resolved `cwd` or a
|
|
5150
|
-
* session's `hostDir`, all of them already absolute. A relative one means a new call site got it
|
|
5151
|
-
* wrong, and a stack trace names that call site; a typed error would ask every caller to handle a
|
|
5152
|
-
* case none of them can cause.
|
|
5153
|
-
*/
|
|
5154
|
-
const rooted = (directory, role) => {
|
|
5155
|
-
if (hostPath.isAbsolute(directory)) return directory;
|
|
5156
|
-
throw new Error(`${role} must be an absolute path, got ${JSON.stringify(directory)}`);
|
|
5157
|
-
};
|
|
5158
|
-
//#endregion
|
|
5159
|
-
//#region src/plugin/npm.ts
|
|
5160
|
-
const Manifest$1 = Schema.Struct({ version: Schema.optional(Schema.String) });
|
|
5161
|
-
const Lockfile = Schema.Struct({ packages: Schema.optional(Schema.Record(Schema.String, Schema.Struct({ resolved: Schema.optional(Schema.String) }))) });
|
|
5162
|
-
/**
|
|
5163
|
-
* npm's flattened config: registry, scopes, auth, and cache paths.
|
|
5164
|
-
*
|
|
5165
|
-
* Built in one place, because two call sites deriving it separately is how the cache location
|
|
5166
|
-
* drifts between an install and the staleness check that is supposed to describe it.
|
|
5167
|
-
*
|
|
5168
|
-
* `dir` is where the `.npmrc` chain is read from, and it is the **host directory the caller
|
|
5169
|
-
* belongs to** -- never the staging directory and never the cache. A company repo keeps its
|
|
5170
|
-
* private registry and its scoped token in its own `.npmrc`, so a chain read anywhere else
|
|
5171
|
-
* resolves the plugin against the public registry and fails 404, or worse, installs a public
|
|
5172
|
-
* package of the same name. This is the registry half of auth; the git half is below.
|
|
5173
|
-
*
|
|
5174
|
-
* No `--prefix`. npm pins its local prefix to a `--prefix` on `argv` and stops walking, so
|
|
5175
|
-
* passing one would look up exactly `<dir>/.npmrc` and nothing above it -- wrong for a person
|
|
5176
|
-
* standing in `packages/x` whose token lives at the repository root. Without it npm walks up from
|
|
5177
|
-
* `cwd` to the nearest `package.json` or `node_modules` exactly as `npm` itself would if it were
|
|
5178
|
-
* run there, which is the only rule a user can predict.
|
|
5179
|
-
*
|
|
5180
|
-
* `--cache` is ours and is passed on `argv`, which outranks every `.npmrc`. npm's `flatten`
|
|
5181
|
-
* derives `_cacache`, `_npx` and `_tuf` from that one value, so naming the parent is enough --
|
|
5182
|
-
* and overriding `flat.cache` afterwards would redirect one of the three and leave `_tuf` pointing
|
|
5183
|
-
* at the user's home. `--home` exists to make a run self-contained; anything reached by an ambient
|
|
5184
|
-
* default is a test reading the developer's real machine.
|
|
5185
|
-
*
|
|
5186
|
-
* **Nothing here names an `env`.** npm hands git `opts.env` when one is present and falls back to
|
|
5187
|
-
* `{ ...gitDefaults, ...process.env }` when it is not, so leaving it out is what lets a git source
|
|
5188
|
-
* reach a private remote at all: `SSH_AUTH_SOCK` finds the agent, `HOME` finds the `~/.gitconfig`
|
|
5189
|
-
* that names the credential helper, and `PATH` finds `git`. Setting `env` to anything -- even to
|
|
5190
|
-
* a copy of `process.env` -- would be a standing invitation to prune it later and break private
|
|
5191
|
-
* git silently, months from the change. §15 Q2.
|
|
5192
|
-
*/
|
|
5193
|
-
const options = (dir, cache) => Effect.suspend(() => {
|
|
5194
|
-
const root = rooted(dir, "the directory a plugin install reads its .npmrc chain from");
|
|
5195
|
-
const store = rooted(cache, "the npm cache");
|
|
5196
|
-
return npmOptions(root, store);
|
|
5197
|
-
});
|
|
5198
|
-
const npmOptions = (dir, cache) => Effect.gen(function* () {
|
|
5199
|
-
const config = yield* Effect.tryPromise(async () => {
|
|
5200
|
-
const { default: Config } = await import("@npmcli/config");
|
|
5201
|
-
const { definitions, flatten, nerfDarts, shorthands } = (await import("@npmcli/config/lib/definitions/index.js")).default;
|
|
5202
|
-
const created = new Config({
|
|
5203
|
-
npmPath: fileURLToPath(new URL("..", import.meta.url)),
|
|
5204
|
-
cwd: dir,
|
|
5205
|
-
env: { ...process.env },
|
|
5206
|
-
argv: [
|
|
5207
|
-
process.execPath,
|
|
5208
|
-
process.execPath,
|
|
5209
|
-
"--cache",
|
|
5210
|
-
hostPath.join(cache, "npm")
|
|
5211
|
-
],
|
|
5212
|
-
execPath: process.execPath,
|
|
5213
|
-
platform: process.platform,
|
|
5214
|
-
definitions,
|
|
5215
|
-
flatten,
|
|
5216
|
-
nerfDarts,
|
|
5217
|
-
shorthands,
|
|
5218
|
-
warn: false
|
|
5219
|
-
});
|
|
5220
|
-
await created.load();
|
|
5221
|
-
return created;
|
|
5222
|
-
}).pipe(Effect.catch((cause) => Effect.logWarning(`cannot load npm configuration from ${dir}; npm defaults apply`, cause).pipe(Effect.as(void 0))));
|
|
5223
|
-
if (config === void 0) return { allowGit: "root" };
|
|
5224
|
-
for (const kind of [
|
|
5225
|
-
"project",
|
|
5226
|
-
"user",
|
|
5227
|
-
"global"
|
|
5228
|
-
]) {
|
|
5229
|
-
const loaded = config.data.get(kind);
|
|
5230
|
-
if (loaded?.loadError !== void 0 && loaded.loadError.code !== "ENOENT") yield* Effect.logWarning(`cannot read npm configuration ${loaded.source ?? `for ${kind}`}; npm defaults apply`, loaded.loadError);
|
|
5231
|
-
}
|
|
5232
|
-
return {
|
|
5233
|
-
...config.flat,
|
|
5234
|
-
allowGit: "root"
|
|
5235
|
-
};
|
|
5236
|
-
});
|
|
5237
|
-
const PUBLIC_REGISTRY = "https://registry.npmjs.org/";
|
|
5238
|
-
/** Stable, credential-free registry identity used to address registry artifacts in the store. */
|
|
5239
|
-
const registry = Effect.fn("PluginNpm.registry")(function* (target, cache, from) {
|
|
5240
|
-
const flat = yield* options(from, cache);
|
|
5241
|
-
const scope = target.name.startsWith("@") ? target.name.slice(0, target.name.indexOf("/")) : void 0;
|
|
5242
|
-
const configured = (scope === void 0 ? void 0 : flat[`${scope}:registry`]) ?? flat.registry ?? PUBLIC_REGISTRY;
|
|
5243
|
-
if (typeof configured !== "string") return yield* new InstallError({
|
|
5244
|
-
reason: "plugin-resolve-failed",
|
|
5245
|
-
reference: target.spec,
|
|
5246
|
-
message: `npm registry for ${target.name} is not a URL`
|
|
5247
|
-
});
|
|
5248
|
-
return yield* Effect.try({
|
|
5249
|
-
try: () => {
|
|
5250
|
-
const value = new URL(configured);
|
|
5251
|
-
value.username = "";
|
|
5252
|
-
value.password = "";
|
|
5253
|
-
value.hash = "";
|
|
5254
|
-
return value.href;
|
|
5255
|
-
},
|
|
5256
|
-
catch: (cause) => new InstallError({
|
|
5257
|
-
reason: "plugin-resolve-failed",
|
|
5258
|
-
reference: target.spec,
|
|
5259
|
-
message: `npm registry for ${target.name} is not a valid URL: ${configured}`,
|
|
5260
|
-
cause
|
|
5261
|
-
})
|
|
5262
|
-
});
|
|
5263
|
-
});
|
|
5264
|
-
/** A 40- or 64-hex commit out of the `resolved` URL arborist writes into the lockfile. */
|
|
5265
|
-
const commitIn = (resolved) => resolved?.match(/#([a-f0-9]{40}|[a-f0-9]{64})(?=::|$)/i)?.[1];
|
|
5266
|
-
/**
|
|
5267
|
-
* The commit a git install actually landed on, read back from the lockfile arborist wrote.
|
|
5268
|
-
*
|
|
5269
|
-
* Both spellings are tried because which one exists depends on whether the tree was saved.
|
|
5270
|
-
*/
|
|
5271
|
-
const commit = Effect.fn("PluginNpm.commit")(function* (root, name) {
|
|
5272
|
-
for (const file of [hostPath.join(root, "package-lock.json"), hostPath.join(root, "node_modules", ".package-lock.json")]) {
|
|
5273
|
-
const parsed = yield* fileSystem.readFileString(file).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Lockfile))), Effect.option);
|
|
5274
|
-
if (Option.isNone(parsed)) continue;
|
|
5275
|
-
const found = commitIn(parsed.value.packages?.[`node_modules/${name}`]?.resolved);
|
|
5276
|
-
if (found !== void 0) return found;
|
|
5277
|
-
}
|
|
5278
|
-
});
|
|
5279
|
-
/** Node error codes that mean "nothing here", as opposed to "this package is broken". */
|
|
5280
|
-
const notFound = /* @__PURE__ */ new Set([
|
|
5281
|
-
"ENOENT",
|
|
5282
|
-
"ENOTDIR",
|
|
5283
|
-
"MODULE_NOT_FOUND",
|
|
5284
|
-
"ERR_MODULE_NOT_FOUND",
|
|
5285
|
-
"ERR_PACKAGE_PATH_NOT_EXPORTED",
|
|
5286
|
-
"ERR_UNSUPPORTED_DIR_IMPORT"
|
|
5287
|
-
]);
|
|
5288
|
-
/**
|
|
5289
|
-
* The module to import out of an installed package.
|
|
5290
|
-
*
|
|
5291
|
-
* Ordered subpaths through the package's own `exports`, so a package that publishes a dedicated
|
|
5292
|
-
* `./plugin` entry is honoured and one that does not falls back to its main.
|
|
5293
|
-
*
|
|
5294
|
-
* Only "not found" is tolerated. Swallowing every error would turn a package that is broken --
|
|
5295
|
-
* a syntax error in its manifest, a permission problem -- into "no entrypoint", which is the
|
|
5296
|
-
* wrong diagnosis in the one place a person needs the right one.
|
|
5297
|
-
*/
|
|
5298
|
-
const entrypoint = (directory, name) => Effect.gen(function* () {
|
|
5299
|
-
for (const subpath of ["", "plugin"]) {
|
|
5300
|
-
const specifier = name === void 0 ? hostPath.join(directory, subpath === "" ? "index" : subpath) : hostPath.join(name, subpath);
|
|
5301
|
-
const resolved = yield* Effect.try(() => resolveModule(specifier, directory)).pipe(Effect.asSome, Effect.catchIf((error) => notFound.has(String(error.cause?.code)), () => Effect.succeed(Option.none())), Effect.mapError((error) => new InstallError({
|
|
5302
|
-
reason: "plugin-no-entrypoint",
|
|
5303
|
-
reference: name ?? directory,
|
|
5304
|
-
message: `cannot resolve an entrypoint in ${directory}`,
|
|
5305
|
-
cause: error
|
|
5306
|
-
})));
|
|
5307
|
-
if (Option.isSome(resolved)) return fileURLToPath(resolved.value);
|
|
5308
|
-
}
|
|
5309
|
-
return yield* new InstallError({
|
|
5310
|
-
reason: "plugin-no-entrypoint",
|
|
5311
|
-
reference: name ?? directory,
|
|
5312
|
-
message: `no plugin entrypoint in ${directory}`
|
|
5313
|
-
});
|
|
5314
|
-
});
|
|
5315
|
-
const reify = Effect.fn("PluginNpm.reify")(function* ({ target, into, cache, from }) {
|
|
5316
|
-
const flat = yield* options(from, cache);
|
|
5317
|
-
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"));
|
|
5318
|
-
const settings = {
|
|
5319
|
-
...flat,
|
|
5320
|
-
...target.mutable ? {
|
|
5321
|
-
preferOnline: true,
|
|
5322
|
-
noGitRevCache: true
|
|
5323
|
-
} : {},
|
|
5324
|
-
audit: false
|
|
5325
|
-
};
|
|
5326
|
-
const arborist = new Arborist({
|
|
5327
|
-
...settings,
|
|
5328
|
-
path: into,
|
|
5329
|
-
binLinks: true,
|
|
5330
|
-
progress: false,
|
|
5331
|
-
savePrefix: "",
|
|
5332
|
-
ignoreScripts: true
|
|
5333
|
-
});
|
|
5334
|
-
return yield* Effect.tryPromise({
|
|
5335
|
-
try: () => arborist.reify({
|
|
5336
|
-
...settings,
|
|
5337
|
-
add: [target.spec],
|
|
5338
|
-
update: target.mutable,
|
|
5339
|
-
save: true,
|
|
5340
|
-
saveType: "prod"
|
|
5341
|
-
}),
|
|
5342
|
-
catch: (cause) => new InstallError({
|
|
5343
|
-
reason: "plugin-fetch-failed",
|
|
5344
|
-
reference: target.spec,
|
|
5345
|
-
message: `cannot install ${target.spec}`,
|
|
5346
|
-
cause
|
|
5347
|
-
})
|
|
5348
|
-
});
|
|
5349
|
-
});
|
|
5350
|
-
/**
|
|
5351
|
-
* Fill `into` with the package `target` names, and report what landed there.
|
|
5080
|
+
* Truncation is based on two independent limits — whichever is hit first
|
|
5081
|
+
* wins: a line limit and a byte limit. Lines are never split (except the bash
|
|
5082
|
+
* tail edge case where a single line exceeds the byte limit).
|
|
5352
5083
|
*
|
|
5353
|
-
*
|
|
5354
|
-
*
|
|
5084
|
+
* The bash tool uses {@link truncateTail} (keep the end — errors / final
|
|
5085
|
+
* results); file readers use {@link truncateHead} (keep the beginning).
|
|
5355
5086
|
*/
|
|
5356
|
-
const
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
target,
|
|
5371
|
-
into: root,
|
|
5372
|
-
cache,
|
|
5373
|
-
from
|
|
5374
|
-
})).edgesOut.values().next().value?.to;
|
|
5375
|
-
if (node === void 0) return yield* new InstallError({
|
|
5376
|
-
reason: "plugin-fetch-failed",
|
|
5377
|
-
reference: target.spec,
|
|
5378
|
-
message: `${target.spec} installed nothing`
|
|
5379
|
-
});
|
|
5380
|
-
const manifest = yield* fileSystem.readFileString(hostPath.join(node.path, "package.json")).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest$1))), Effect.option);
|
|
5381
|
-
const version = Option.isNone(manifest) ? void 0 : manifest.value.version;
|
|
5382
|
-
const revision = target.kind === "registry" ? version : (yield* commit(root, node.name)) ?? (yield* noCommit(target.spec));
|
|
5383
|
-
return {
|
|
5384
|
-
directory: node.path,
|
|
5385
|
-
name: node.name,
|
|
5386
|
-
...version === void 0 ? {} : { version },
|
|
5387
|
-
...revision === void 0 ? {} : { revision },
|
|
5388
|
-
entrypoint: yield* entrypoint(node.path, node.name)
|
|
5389
|
-
};
|
|
5390
|
-
});
|
|
5391
|
-
const noCommit = (spec) => new InstallError({
|
|
5392
|
-
reason: "plugin-no-commit",
|
|
5393
|
-
reference: spec,
|
|
5394
|
-
message: `cannot resolve a commit for ${spec}`
|
|
5395
|
-
});
|
|
5087
|
+
const DEFAULT_MAX_LINES = 2e3;
|
|
5088
|
+
const DEFAULT_MAX_BYTES = 51200;
|
|
5089
|
+
function splitLinesForCounting(content) {
|
|
5090
|
+
if (content.length === 0) return [];
|
|
5091
|
+
const lines = content.split("\n");
|
|
5092
|
+
if (content.endsWith("\n")) lines.pop();
|
|
5093
|
+
return lines;
|
|
5094
|
+
}
|
|
5095
|
+
/** Format bytes as a human-readable size. */
|
|
5096
|
+
function formatSize(bytes) {
|
|
5097
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
5098
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
5099
|
+
return `${(bytes / 1048576).toFixed(1)}MB`;
|
|
5100
|
+
}
|
|
5396
5101
|
/**
|
|
5397
|
-
*
|
|
5398
|
-
*
|
|
5399
|
-
*
|
|
5400
|
-
*
|
|
5401
|
-
* target skips the call entirely -- a version and a SHA cannot move.
|
|
5102
|
+
* Truncate content from the tail (keep the last N lines/bytes). Suitable for
|
|
5103
|
+
* bash output where the end matters (errors, final results). May return a
|
|
5104
|
+
* partial first line if the last line of the original content exceeds the byte
|
|
5105
|
+
* limit.
|
|
5402
5106
|
*/
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
const
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5107
|
+
function truncateTail(content, options = {}) {
|
|
5108
|
+
const maxLines = options.maxLines ?? 2e3;
|
|
5109
|
+
const maxBytes = options.maxBytes ?? 51200;
|
|
5110
|
+
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
5111
|
+
const lines = splitLinesForCounting(content);
|
|
5112
|
+
const totalLines = lines.length;
|
|
5113
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) return {
|
|
5114
|
+
content,
|
|
5115
|
+
truncated: false,
|
|
5116
|
+
truncatedBy: null,
|
|
5117
|
+
totalLines,
|
|
5118
|
+
totalBytes,
|
|
5119
|
+
outputLines: totalLines,
|
|
5120
|
+
outputBytes: totalBytes,
|
|
5121
|
+
lastLinePartial: false,
|
|
5122
|
+
firstLineExceedsLimit: false,
|
|
5123
|
+
maxLines,
|
|
5124
|
+
maxBytes
|
|
5410
5125
|
};
|
|
5411
|
-
const
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
import * as import__codeworksh_plugin_plugin from "@codeworksh/plugin/plugin";
|
|
5428
|
-
__reExport(plugin_exports$1, import__codeworksh_plugin_plugin);
|
|
5429
|
-
//#endregion
|
|
5430
|
-
//#region src/plugin/source.ts
|
|
5431
|
-
/** A committish that names one artifact rather than a moving branch: an unabbreviated SHA. */
|
|
5432
|
-
const isCommit = (committish) => typeof committish === "string" && /^[a-f0-9]{40}$|^[a-f0-9]{64}$/i.test(committish);
|
|
5433
|
-
/**
|
|
5434
|
-
* A short, readable name for a git source, used to make a store path recognisable to a human
|
|
5435
|
-
* reading `ls`. It is never an identity: two remotes can slug alike, and the digest is what
|
|
5436
|
-
* separates them.
|
|
5437
|
-
*/
|
|
5438
|
-
const slug = (spec) => {
|
|
5439
|
-
const withoutCommittish = spec.split("#")[0] ?? spec;
|
|
5440
|
-
return (() => {
|
|
5441
|
-
try {
|
|
5442
|
-
return decodeURIComponent(withoutCommittish);
|
|
5443
|
-
} catch {
|
|
5444
|
-
return withoutCommittish;
|
|
5126
|
+
const outputLinesArr = [];
|
|
5127
|
+
let outputBytesCount = 0;
|
|
5128
|
+
let truncatedBy = "lines";
|
|
5129
|
+
let lastLinePartial = false;
|
|
5130
|
+
for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
|
|
5131
|
+
const line = lines[i] ?? "";
|
|
5132
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0);
|
|
5133
|
+
if (outputBytesCount + lineBytes > maxBytes) {
|
|
5134
|
+
truncatedBy = "bytes";
|
|
5135
|
+
if (outputLinesArr.length === 0) {
|
|
5136
|
+
const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
|
|
5137
|
+
outputLinesArr.unshift(truncatedLine);
|
|
5138
|
+
outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
|
|
5139
|
+
lastLinePartial = true;
|
|
5140
|
+
}
|
|
5141
|
+
break;
|
|
5445
5142
|
}
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5143
|
+
outputLinesArr.unshift(line);
|
|
5144
|
+
outputBytesCount += lineBytes;
|
|
5145
|
+
}
|
|
5146
|
+
if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) truncatedBy = "lines";
|
|
5147
|
+
const outputContent = outputLinesArr.join("\n");
|
|
5148
|
+
const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
|
|
5149
|
+
return {
|
|
5150
|
+
content: outputContent,
|
|
5151
|
+
truncated: true,
|
|
5152
|
+
truncatedBy,
|
|
5153
|
+
totalLines,
|
|
5154
|
+
totalBytes,
|
|
5155
|
+
outputLines: outputLinesArr.length,
|
|
5156
|
+
outputBytes: finalOutputBytes,
|
|
5157
|
+
lastLinePartial,
|
|
5158
|
+
firstLineExceedsLimit: false,
|
|
5159
|
+
maxLines,
|
|
5160
|
+
maxBytes
|
|
5161
|
+
};
|
|
5162
|
+
}
|
|
5450
5163
|
/**
|
|
5451
|
-
*
|
|
5452
|
-
*
|
|
5453
|
-
* - a **plugin entry** anchors to the directory holding the file that declared it --
|
|
5454
|
-
* `<root>/.codework/` for a project entry, `<home>/` for a user one, the named directory for
|
|
5455
|
-
* `--user-config-dir`. The rule is about the file, not the project.
|
|
5456
|
-
* - a **CLI argument** anchors to the process's startup directory, because that is where the
|
|
5457
|
-
* person typed it.
|
|
5164
|
+
* Truncate a string to fit within a byte limit (from the end). Handles
|
|
5165
|
+
* multi-byte UTF-8 characters correctly (never splits a codepoint).
|
|
5458
5166
|
*/
|
|
5459
|
-
|
|
5460
|
-
const
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5167
|
+
function truncateStringToBytesFromEnd(str, maxBytes) {
|
|
5168
|
+
const buf = Buffer.from(str, "utf-8");
|
|
5169
|
+
if (buf.length <= maxBytes) return str;
|
|
5170
|
+
let start = buf.length - maxBytes;
|
|
5171
|
+
while (start < buf.length && (buf[start] & 192) === 128) start++;
|
|
5172
|
+
return buf.subarray(start).toString("utf-8");
|
|
5173
|
+
}
|
|
5174
|
+
//#endregion
|
|
5175
|
+
//#region src/tool/accumulator.ts
|
|
5176
|
+
function defaultTempFilePath(prefix, suffix) {
|
|
5177
|
+
return posix.join(tmpdir(), `${prefix}-${suffix}.log`);
|
|
5178
|
+
}
|
|
5179
|
+
function byteLength(text) {
|
|
5180
|
+
return Buffer.byteLength(text, "utf-8");
|
|
5181
|
+
}
|
|
5182
|
+
var Accumulator = class {
|
|
5183
|
+
maxLines;
|
|
5184
|
+
maxBytes;
|
|
5185
|
+
maxRollingBytes;
|
|
5186
|
+
tempFilePrefix;
|
|
5187
|
+
decoder = new TextDecoder();
|
|
5188
|
+
rawChunks = [];
|
|
5189
|
+
tailText = "";
|
|
5190
|
+
tailBytes = 0;
|
|
5191
|
+
tailStartsAtLineBoundary = true;
|
|
5192
|
+
totalRawBytes = 0;
|
|
5193
|
+
totalDecodedBytes = 0;
|
|
5194
|
+
completedLines = 0;
|
|
5195
|
+
totalLines = 0;
|
|
5196
|
+
currentLineBytes = 0;
|
|
5197
|
+
hasOpenLine = false;
|
|
5198
|
+
finished = false;
|
|
5199
|
+
tempFilePath;
|
|
5200
|
+
tempFile;
|
|
5201
|
+
constructor(options = {}) {
|
|
5202
|
+
this.maxLines = options.maxLines ?? 2e3;
|
|
5203
|
+
this.maxBytes = options.maxBytes ?? 51200;
|
|
5204
|
+
this.maxRollingBytes = Math.max(this.maxBytes * 2, 1);
|
|
5205
|
+
this.tempFilePrefix = options.tempFilePrefix ?? "codework-output";
|
|
5472
5206
|
}
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
kind: "git",
|
|
5486
|
-
slug: slug(reference),
|
|
5487
|
-
spec: reference,
|
|
5488
|
-
...parsed.gitCommittish == null ? {} : { committish: parsed.gitCommittish },
|
|
5489
|
-
...gitSubdir == null ? {} : { subdir: gitSubdir },
|
|
5490
|
-
mutable: !isCommit(parsed.gitCommittish)
|
|
5491
|
-
};
|
|
5207
|
+
append(data) {
|
|
5208
|
+
return Effect.suspend(() => {
|
|
5209
|
+
if (this.finished) return Effect.die(/* @__PURE__ */ new Error("cannot append to a finished output accumulator"));
|
|
5210
|
+
this.totalRawBytes += data.length;
|
|
5211
|
+
this.appendDecodedText(this.decoder.decode(data, { stream: true }));
|
|
5212
|
+
if (this.tempFile !== void 0 || this.shouldUseTempFile()) return Effect.gen({ self: this }, function* () {
|
|
5213
|
+
yield* this.ensureTempFile();
|
|
5214
|
+
yield* this.tempFile.writeAll(data).pipe(Effect.orDie);
|
|
5215
|
+
});
|
|
5216
|
+
if (data.length > 0) this.rawChunks.push(data);
|
|
5217
|
+
return Effect.void;
|
|
5218
|
+
});
|
|
5492
5219
|
}
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5220
|
+
finish() {
|
|
5221
|
+
return Effect.suspend(() => {
|
|
5222
|
+
if (this.finished) return Effect.void;
|
|
5223
|
+
this.finished = true;
|
|
5224
|
+
this.appendDecodedText(this.decoder.decode());
|
|
5225
|
+
return Effect.gen({ self: this }, function* () {
|
|
5226
|
+
if (this.shouldUseTempFile()) yield* this.ensureTempFile();
|
|
5227
|
+
if (this.tempFile !== void 0) yield* this.tempFile.sync.pipe(Effect.orDie);
|
|
5228
|
+
});
|
|
5229
|
+
});
|
|
5230
|
+
}
|
|
5231
|
+
snapshot() {
|
|
5232
|
+
const tailTruncation = truncateTail(this.getSnapshotText(), {
|
|
5233
|
+
maxLines: this.maxLines,
|
|
5234
|
+
maxBytes: this.maxBytes
|
|
5235
|
+
});
|
|
5236
|
+
const truncated = this.totalLines > this.maxLines || this.totalDecodedBytes > this.maxBytes;
|
|
5237
|
+
const truncatedBy = truncated ? tailTruncation.truncatedBy ?? (this.totalDecodedBytes > this.maxBytes ? "bytes" : "lines") : null;
|
|
5238
|
+
const truncation = {
|
|
5239
|
+
...tailTruncation,
|
|
5240
|
+
truncated,
|
|
5241
|
+
truncatedBy,
|
|
5242
|
+
totalLines: this.totalLines,
|
|
5243
|
+
totalBytes: this.totalDecodedBytes,
|
|
5244
|
+
maxLines: this.maxLines,
|
|
5245
|
+
maxBytes: this.maxBytes
|
|
5246
|
+
};
|
|
5499
5247
|
return {
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
mutable: parsed.type !== "version"
|
|
5248
|
+
content: truncation.content,
|
|
5249
|
+
truncation,
|
|
5250
|
+
...this.tempFilePath === void 0 ? {} : { fullOutputPath: this.tempFilePath }
|
|
5504
5251
|
};
|
|
5505
5252
|
}
|
|
5506
|
-
|
|
5507
|
-
|
|
5253
|
+
/** Bytes in the final (possibly unterminated) line — for the partial-line footer. */
|
|
5254
|
+
getLastLineBytes() {
|
|
5255
|
+
return this.currentLineBytes;
|
|
5256
|
+
}
|
|
5257
|
+
appendDecodedText(text) {
|
|
5258
|
+
if (text.length === 0) return;
|
|
5259
|
+
const bytes = byteLength(text);
|
|
5260
|
+
this.totalDecodedBytes += bytes;
|
|
5261
|
+
this.tailText += text;
|
|
5262
|
+
this.tailBytes += bytes;
|
|
5263
|
+
if (this.tailBytes > this.maxRollingBytes * 2) this.trimTail();
|
|
5264
|
+
let newlines = 0;
|
|
5265
|
+
let lastNewline = -1;
|
|
5266
|
+
for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) {
|
|
5267
|
+
newlines++;
|
|
5268
|
+
lastNewline = i;
|
|
5269
|
+
}
|
|
5270
|
+
if (newlines === 0) {
|
|
5271
|
+
this.currentLineBytes += bytes;
|
|
5272
|
+
this.hasOpenLine = true;
|
|
5273
|
+
} else {
|
|
5274
|
+
this.completedLines += newlines;
|
|
5275
|
+
const tail = text.slice(lastNewline + 1);
|
|
5276
|
+
this.currentLineBytes = byteLength(tail);
|
|
5277
|
+
this.hasOpenLine = tail.length > 0;
|
|
5278
|
+
}
|
|
5279
|
+
this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);
|
|
5280
|
+
}
|
|
5281
|
+
trimTail() {
|
|
5282
|
+
const buffer = Buffer.from(this.tailText, "utf-8");
|
|
5283
|
+
if (buffer.length <= this.maxRollingBytes) {
|
|
5284
|
+
this.tailBytes = buffer.length;
|
|
5285
|
+
return;
|
|
5286
|
+
}
|
|
5287
|
+
let start = buffer.length - this.maxRollingBytes;
|
|
5288
|
+
while (start < buffer.length && (buffer[start] & 192) === 128) start++;
|
|
5289
|
+
this.tailStartsAtLineBoundary = start === 0 ? this.tailStartsAtLineBoundary : buffer[start - 1] === 10;
|
|
5290
|
+
this.tailText = buffer.subarray(start).toString("utf-8");
|
|
5291
|
+
this.tailBytes = byteLength(this.tailText);
|
|
5292
|
+
}
|
|
5293
|
+
getSnapshotText() {
|
|
5294
|
+
if (this.tailStartsAtLineBoundary) return this.tailText;
|
|
5295
|
+
const firstNewline = this.tailText.indexOf("\n");
|
|
5296
|
+
return firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1);
|
|
5297
|
+
}
|
|
5298
|
+
shouldUseTempFile() {
|
|
5299
|
+
return this.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines;
|
|
5300
|
+
}
|
|
5301
|
+
ensureTempFile() {
|
|
5302
|
+
if (this.tempFile !== void 0) return Effect.void;
|
|
5303
|
+
return Effect.gen({ self: this }, function* () {
|
|
5304
|
+
const suffix = Encoding.encodeHex(yield* crypto.randomBytes(8));
|
|
5305
|
+
this.tempFilePath = defaultTempFilePath(this.tempFilePrefix, suffix);
|
|
5306
|
+
this.tempFile = yield* fileSystem.open(this.tempFilePath, { flag: "w" });
|
|
5307
|
+
for (const chunk of this.rawChunks) yield* this.tempFile.writeAll(chunk);
|
|
5308
|
+
this.rawChunks = [];
|
|
5309
|
+
}).pipe(Effect.orDie);
|
|
5310
|
+
}
|
|
5311
|
+
};
|
|
5312
|
+
//#endregion
|
|
5313
|
+
//#region src/tool/progress.ts
|
|
5314
|
+
var progress_exports = /* @__PURE__ */ __exportAll({});
|
|
5315
|
+
import * as import__codeworksh_plugin_tool_progress from "@codeworksh/plugin/tool/progress";
|
|
5316
|
+
__reExport(progress_exports, import__codeworksh_plugin_tool_progress);
|
|
5317
|
+
//#endregion
|
|
5318
|
+
//#region src/tool/shell.ts
|
|
5319
|
+
/**
|
|
5320
|
+
* `ToolShell` — the tool-owned, cancellable command-execution capability the bash tool depends on.
|
|
5321
|
+
*
|
|
5322
|
+
* It deliberately does **not** reuse `sandbox/Shell` directly: the local OS path
|
|
5323
|
+
* provides no `Shell` at all, and `sandbox/Shell` models neither cancellation
|
|
5324
|
+
* nor a timeout. `ToolShell` makes both part of the contract. Adapters honor the
|
|
5325
|
+
* mid-flight process kill where the SDK allows it (local, Vercel) and fall back
|
|
5326
|
+
* to the timeout where it does not (Daytona) — "kill where it's easy".
|
|
5327
|
+
*
|
|
5328
|
+
* Two modes, and Local has two variants:
|
|
5329
|
+
* - Local / full OS shell → {@link local} (default)
|
|
5330
|
+
* - Local / just-bash → {@link fromSandboxShell} over `EnvBash`
|
|
5331
|
+
* - Remote / provider → {@link fromSandboxShell} over `EnvDaytona` / `EnvVercel`
|
|
5332
|
+
*/
|
|
5333
|
+
/** Infra/spawn failure — not model-actionable; handlers should treat it as a defect. */
|
|
5334
|
+
var ToolShellError = class extends Schema.TaggedError()("ToolShellError", {
|
|
5335
|
+
command: Schema.String,
|
|
5336
|
+
cause: Schema.optional(Schema.Defect())
|
|
5337
|
+
}) {};
|
|
5338
|
+
/** The command exceeded its deadline. Distinct so handlers can map it to a domain timeout. */
|
|
5339
|
+
var ToolShellTimeout = class extends Schema.TaggedError()("ToolShellTimeout", {
|
|
5340
|
+
command: Schema.String,
|
|
5341
|
+
timeoutMillis: Schema.Finite
|
|
5342
|
+
}) {};
|
|
5343
|
+
const widenExecError = (effect) => effect.pipe(Effect.mapError((error) => error));
|
|
5344
|
+
var ToolShell = class extends Context.Service()("@codeworksh/harness/tool/shell/ToolShell") {};
|
|
5508
5345
|
/**
|
|
5509
|
-
*
|
|
5346
|
+
* Bridge the existing `sandbox/Shell` into a `ToolShell`. Used for just-bash
|
|
5347
|
+
* (in-process) and remote providers.
|
|
5510
5348
|
*
|
|
5511
|
-
*
|
|
5512
|
-
*
|
|
5349
|
+
* `cwd` and `env` both pass through to the backing `sandbox/Shell`, which
|
|
5350
|
+
* resolves a relative `cwd` against its mount. The buffered `exec`
|
|
5351
|
+
* timeout is enforced here with `Effect.timeoutOption` (interruption always
|
|
5352
|
+
* returns control; the underlying command is only truly killed mid-flight on
|
|
5353
|
+
* signal-aware backends, handled in the provider layer). When the backend exposes
|
|
5354
|
+
* `stream` (e.g. Vercel `Command.logs`), it is bridged to {@link IToolShell.stream}
|
|
5355
|
+
* so the bash tool streams over it too.
|
|
5513
5356
|
*/
|
|
5514
|
-
const
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
}
|
|
5357
|
+
const fromSandboxShell = Layer.effect(ToolShell, Effect.gen(function* () {
|
|
5358
|
+
const shell = yield* shell_exports.Shell;
|
|
5359
|
+
const exec = (command, options) => {
|
|
5360
|
+
const run = shell.exec(command, {
|
|
5361
|
+
...options?.env ? { env: options.env } : {},
|
|
5362
|
+
...options?.cwd ? { cwd: options.cwd } : {}
|
|
5363
|
+
}).pipe(Effect.mapError((cause) => new ToolShellError({
|
|
5364
|
+
command,
|
|
5365
|
+
cause
|
|
5366
|
+
})));
|
|
5367
|
+
if (options?.timeout === void 0) return widenExecError(run);
|
|
5368
|
+
const timeout = options.timeout;
|
|
5369
|
+
return run.pipe(Effect.timeoutOption(timeout), Effect.flatMap(Option.match({
|
|
5370
|
+
onNone: () => Effect.fail(new ToolShellTimeout({
|
|
5371
|
+
command,
|
|
5372
|
+
timeoutMillis: Duration.toMillis(timeout)
|
|
5373
|
+
})),
|
|
5374
|
+
onSome: Effect.succeed
|
|
5375
|
+
})));
|
|
5376
|
+
};
|
|
5377
|
+
const sandboxStream = shell.stream;
|
|
5378
|
+
const stream = sandboxStream ? (command, options) => sandboxStream(command, {
|
|
5379
|
+
...options?.env ? { env: options.env } : {},
|
|
5380
|
+
...options?.cwd ? { cwd: options.cwd } : {}
|
|
5381
|
+
}).pipe(Stream.map((chunk) => chunk._tag === "exit" ? {
|
|
5382
|
+
_tag: "Exit",
|
|
5383
|
+
exitCode: chunk.exitCode
|
|
5384
|
+
} : {
|
|
5385
|
+
_tag: "Output",
|
|
5386
|
+
bytes: chunk.bytes
|
|
5387
|
+
}), Stream.mapError((cause) => new ToolShellError({
|
|
5388
|
+
command,
|
|
5389
|
+
cause
|
|
5390
|
+
}))) : void 0;
|
|
5391
|
+
return ToolShell.of(stream ? {
|
|
5392
|
+
exec,
|
|
5393
|
+
stream
|
|
5394
|
+
} : { exec });
|
|
5395
|
+
}));
|
|
5396
|
+
Duration.seconds(1);
|
|
5521
5397
|
//#endregion
|
|
5522
|
-
//#region src/
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
mutable: Schema.Boolean,
|
|
5529
|
-
generation: Schema.Finite,
|
|
5530
|
-
version: Schema.optional(Schema.String),
|
|
5531
|
-
revision: Schema.optional(Schema.String),
|
|
5532
|
-
entrypoint: Schema.String,
|
|
5533
|
-
installedAt: Schema.Finite,
|
|
5534
|
-
checkedAt: Schema.optional(Schema.Finite),
|
|
5535
|
-
outdated: Schema.optional(Schema.Boolean),
|
|
5536
|
-
/** The remote revision observed at `checkedAt`; distinct from the installed `revision`. */
|
|
5537
|
-
available: Schema.optional(Schema.String)
|
|
5538
|
-
});
|
|
5539
|
-
const File = Schema.Struct({
|
|
5540
|
-
version: Schema.Finite,
|
|
5541
|
-
entries: Schema.Record(Schema.String, Record_)
|
|
5542
|
-
});
|
|
5543
|
-
const VERSION = 1;
|
|
5544
|
-
const NAME = "index.json";
|
|
5398
|
+
//#region src/tool/tool.ts
|
|
5399
|
+
var tool_exports = /* @__PURE__ */ __exportAll({});
|
|
5400
|
+
import * as import__codeworksh_plugin_tool from "@codeworksh/plugin/tool";
|
|
5401
|
+
__reExport(tool_exports, import__codeworksh_plugin_tool);
|
|
5402
|
+
//#endregion
|
|
5403
|
+
//#region src/plugin/builtin/tool/bash.ts
|
|
5545
5404
|
/**
|
|
5546
|
-
*
|
|
5405
|
+
* The self-contained bash plugin — a worked example for tool plugins. The definition is pure data;
|
|
5406
|
+
* the handler depends only on {@link ToolShell} / {@link ToolProgress}, never
|
|
5407
|
+
* `sandbox/Shell`, so the backend (local OS / just-bash / remote provider) is
|
|
5408
|
+
* swapped by changing the provided Layer with no change to the tool.
|
|
5547
5409
|
*
|
|
5548
|
-
*
|
|
5549
|
-
*
|
|
5410
|
+
* Two execution paths, picked by capability:
|
|
5411
|
+
* - **variant A (buffered)** otherwise: `ToolShell.exec` returns the full output,
|
|
5412
|
+
* truncated once after completion.
|
|
5413
|
+
* - **variant B (streaming)** when the backend offers `ToolShell.stream`: output
|
|
5414
|
+
* flows through an {@link Accumulator} (bounded memory + temp-file spill)
|
|
5415
|
+
* and is reported live via {@link ToolProgress}; on timeout the partial output
|
|
5416
|
+
* produced so far is preserved.
|
|
5417
|
+
*
|
|
5418
|
+
* Success and failure carry the *same* structured shape
|
|
5419
|
+
* (`output`, `exitCode`, `truncated`, `fullOutputPath`)
|
|
5420
|
+
* — a non-zero exit is just `exitCode !== 0`, not a different kind of result.
|
|
5550
5421
|
*/
|
|
5551
|
-
const
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
return new Map(Object.entries(parsed.value.entries));
|
|
5422
|
+
const BashParams = Schema.Struct({
|
|
5423
|
+
command: Schema.String.annotate({ description: "The bash command to execute." }),
|
|
5424
|
+
timeout: Schema.optional(Schema.Finite.check(Schema.isGreaterThan(0)).annotate({ description: "Optional timeout in seconds (must be greater than 0)." }))
|
|
5555
5425
|
});
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5426
|
+
const outputFields = {
|
|
5427
|
+
/** Combined stdout + stderr, truncated for display (full output is on disk when `truncated`). */
|
|
5428
|
+
output: Schema.String,
|
|
5429
|
+
truncated: Schema.Boolean,
|
|
5430
|
+
fullOutputPath: Schema.optional(Schema.String)
|
|
5431
|
+
};
|
|
5432
|
+
const BashSuccess = Schema.Struct({
|
|
5433
|
+
...outputFields,
|
|
5434
|
+
exitCode: Schema.Finite
|
|
5435
|
+
});
|
|
5436
|
+
/** Non-zero exit — expected, model-visible. Carries the same shape as success. */
|
|
5437
|
+
var BashFailed = class extends Schema.TaggedError()("BashFailed", {
|
|
5438
|
+
...outputFields,
|
|
5439
|
+
exitCode: Schema.Finite
|
|
5440
|
+
}) {};
|
|
5441
|
+
/** Deadline exceeded — expected, model-visible. Carries the partial output produced so far. */
|
|
5442
|
+
var BashTimedOut = class extends Schema.TaggedError()("BashTimedOut", {
|
|
5443
|
+
...outputFields,
|
|
5444
|
+
timeoutSeconds: Schema.Finite
|
|
5445
|
+
}) {};
|
|
5446
|
+
const BashFailure = Schema.Union([BashFailed, BashTimedOut]);
|
|
5447
|
+
const bashDef = tool_exports.define({
|
|
5448
|
+
name: "bash",
|
|
5449
|
+
label: "bash",
|
|
5450
|
+
promptSnippet: "Execute bash commands (ls, grep, find, etc.).",
|
|
5451
|
+
description: `Execute a bash command in the working directory and return its combined stdout/stderr output. Output is truncated to the last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES}KB (whichever is hit first); when truncated, the full output is saved to a temp file. A non-zero exit code is reported as an error carrying the captured output.`,
|
|
5452
|
+
parameters: BashParams,
|
|
5453
|
+
success: BashSuccess,
|
|
5454
|
+
failure: BashFailure,
|
|
5455
|
+
encodeContent: (success) => [{
|
|
5456
|
+
type: "text",
|
|
5457
|
+
text: success.output
|
|
5458
|
+
}],
|
|
5459
|
+
encodeFailureContent: (failure) => [{
|
|
5460
|
+
type: "text",
|
|
5461
|
+
text: failure.output
|
|
5462
|
+
}]
|
|
5463
|
+
});
|
|
5464
|
+
/** Combine stdout and stderr into one stream of text, stderr after stdout. */
|
|
5465
|
+
const combineOutput = (stdout, stderr) => {
|
|
5466
|
+
if (stderr.length === 0) return stdout;
|
|
5467
|
+
if (stdout.length === 0) return stderr;
|
|
5468
|
+
return `${stdout}\n${stderr}`;
|
|
5469
|
+
};
|
|
5470
|
+
/** One-line footer appended to truncated, model-facing output. */
|
|
5471
|
+
const footer = (t, fullOutputPath, lastLineBytes) => {
|
|
5472
|
+
const where = fullOutputPath ? ` Full output: ${fullOutputPath}` : "";
|
|
5473
|
+
const startLine = t.totalLines - t.outputLines + 1;
|
|
5474
|
+
if (t.lastLinePartial) {
|
|
5475
|
+
const lineSize = lastLineBytes !== void 0 ? ` (line is ${formatSize(lastLineBytes)})` : "";
|
|
5476
|
+
return `\n\n[showing last ${formatSize(t.outputBytes)} of line ${t.totalLines}${lineSize}.${where}]`;
|
|
5477
|
+
}
|
|
5478
|
+
if (t.truncatedBy === "lines") return `\n\n[showing lines ${startLine}-${t.totalLines} of ${t.totalLines}.${where}]`;
|
|
5479
|
+
return `\n\n[showing lines ${startLine}-${t.totalLines} of ${t.totalLines} (${formatSize(t.maxBytes)} limit).${where}]`;
|
|
5480
|
+
};
|
|
5481
|
+
/** Write full output to a host temp file (best-effort; undefined on failure). */
|
|
5482
|
+
const spillToTempFile = (content) => Effect.gen(function* () {
|
|
5483
|
+
const suffix = Encoding.encodeHex(yield* crypto.randomBytes(6));
|
|
5484
|
+
const path = posix.join(tmpdir(), `codework-bash-${suffix}.log`);
|
|
5485
|
+
yield* fileSystem.writeFileString(path, content);
|
|
5486
|
+
return path;
|
|
5487
|
+
}).pipe(Effect.orElseSucceed(() => void 0));
|
|
5488
|
+
/** Variant A: truncate the buffered output once, spilling the full output if truncated. */
|
|
5489
|
+
const presentBuffered = (combined) => Effect.gen(function* () {
|
|
5490
|
+
const t = truncateTail(combined);
|
|
5491
|
+
if (!t.truncated) return {
|
|
5492
|
+
output: t.content,
|
|
5493
|
+
truncated: false
|
|
5494
|
+
};
|
|
5495
|
+
const fullOutputPath = yield* spillToTempFile(combined);
|
|
5496
|
+
return {
|
|
5497
|
+
output: t.content + footer(t, fullOutputPath),
|
|
5498
|
+
truncated: true,
|
|
5499
|
+
...fullOutputPath !== void 0 ? { fullOutputPath } : {}
|
|
5500
|
+
};
|
|
5501
|
+
});
|
|
5502
|
+
/** Variant B: present an accumulator snapshot (the temp file is spilled incrementally). */
|
|
5503
|
+
const presentSnapshot = (snap, lastLineBytes) => {
|
|
5504
|
+
if (!snap.truncation.truncated) return {
|
|
5505
|
+
output: snap.content,
|
|
5506
|
+
truncated: false
|
|
5507
|
+
};
|
|
5508
|
+
return {
|
|
5509
|
+
output: snap.content + footer(snap.truncation, snap.fullOutputPath, lastLineBytes),
|
|
5510
|
+
truncated: true,
|
|
5511
|
+
...snap.fullOutputPath !== void 0 ? { fullOutputPath: snap.fullOutputPath } : {}
|
|
5512
|
+
};
|
|
5513
|
+
};
|
|
5514
|
+
/** Variant A — buffered: one `exec`, truncate after completion. No live progress. */
|
|
5515
|
+
const runBuffered = (shell, params) => Effect.gen(function* () {
|
|
5516
|
+
const result = yield* shell.exec(params.command, params.timeout !== void 0 ? { timeout: Duration.seconds(params.timeout) } : void 0).pipe(Effect.catchTags({
|
|
5517
|
+
ToolShellTimeout: (timeout) => Effect.fail(new BashTimedOut({
|
|
5518
|
+
timeoutSeconds: timeout.timeoutMillis / 1e3,
|
|
5519
|
+
output: "",
|
|
5520
|
+
truncated: false
|
|
5521
|
+
})),
|
|
5522
|
+
ToolShellError: (cause) => Effect.die(cause)
|
|
5523
|
+
}));
|
|
5524
|
+
const present = yield* presentBuffered(combineOutput(result.stdout, result.stderr));
|
|
5525
|
+
if (result.exitCode !== 0) return yield* new BashFailed({
|
|
5526
|
+
exitCode: result.exitCode,
|
|
5527
|
+
...present
|
|
5561
5528
|
});
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5529
|
+
return {
|
|
5530
|
+
exitCode: result.exitCode,
|
|
5531
|
+
...present
|
|
5532
|
+
};
|
|
5533
|
+
});
|
|
5534
|
+
/** Variant B — streaming: accumulate output, report progress, keep partial output on timeout. */
|
|
5535
|
+
const runStreaming = (stream, params) => Effect.scoped(Effect.gen(function* () {
|
|
5536
|
+
const acc = new Accumulator({ tempFilePrefix: "codework-bash" });
|
|
5537
|
+
const progress = yield* progress_exports.ToolProgress;
|
|
5538
|
+
const exitCode = yield* Ref.make(null);
|
|
5539
|
+
const consume = stream(params.command).pipe(Stream.runForEach((event) => event._tag === "Exit" ? Ref.set(exitCode, event.exitCode) : Effect.gen(function* () {
|
|
5540
|
+
yield* acc.append(Buffer.from(event.bytes));
|
|
5541
|
+
yield* progress.report({ content: [{
|
|
5542
|
+
type: "text",
|
|
5543
|
+
text: acc.snapshot().content
|
|
5544
|
+
}] });
|
|
5545
|
+
})), Effect.catchTag("ToolShellError", (cause) => Effect.die(cause)));
|
|
5546
|
+
let timedOutAfter;
|
|
5547
|
+
if (params.timeout !== void 0) {
|
|
5548
|
+
const finished = yield* consume.pipe(Effect.timeoutOption(Duration.seconds(params.timeout)));
|
|
5549
|
+
if (Option.isNone(finished)) timedOutAfter = params.timeout;
|
|
5550
|
+
} else yield* consume;
|
|
5551
|
+
yield* acc.finish();
|
|
5552
|
+
const present = presentSnapshot(acc.snapshot(), acc.getLastLineBytes());
|
|
5553
|
+
if (timedOutAfter !== void 0) return yield* new BashTimedOut({
|
|
5554
|
+
timeoutSeconds: timedOutAfter,
|
|
5555
|
+
...present
|
|
5567
5556
|
});
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
*
|
|
5573
|
-
* Ignores its own failures. A store that cannot write its accelerator still works; refusing the
|
|
5574
|
-
* install would be the accelerator deciding whether the install counted.
|
|
5575
|
-
*/
|
|
5576
|
-
const put = Effect.fn("PluginIndex.put")(function* (root, digest, record) {
|
|
5577
|
-
const entries = yield* read(root);
|
|
5578
|
-
entries.set(digest, record);
|
|
5579
|
-
yield* write(root, entries).pipe(Effect.ignore);
|
|
5580
|
-
});
|
|
5581
|
-
/**
|
|
5582
|
-
* Merge fields into one record, if it is there.
|
|
5583
|
-
*
|
|
5584
|
-
* Unlocked, like every write here but the one that publishes a generation. Two processes checking
|
|
5585
|
-
* at once both read the whole file and both rewrite it, so one update is lost and a record added
|
|
5586
|
-
* in between can vanish -- and neither matters, because a lost `checkedAt` costs one network call
|
|
5587
|
-
* and a vanished record is a miss a scan repairs.
|
|
5588
|
-
*/
|
|
5589
|
-
const patch = Effect.fn("PluginIndex.patch")(function* (root, digest, fields) {
|
|
5590
|
-
const entries = yield* read(root);
|
|
5591
|
-
const existing = entries.get(digest);
|
|
5592
|
-
if (existing === void 0) return;
|
|
5593
|
-
entries.set(digest, {
|
|
5594
|
-
...existing,
|
|
5595
|
-
...fields
|
|
5557
|
+
const code = yield* Ref.get(exitCode);
|
|
5558
|
+
if (code === null || code !== 0) return yield* new BashFailed({
|
|
5559
|
+
exitCode: code ?? -1,
|
|
5560
|
+
...present
|
|
5596
5561
|
});
|
|
5597
|
-
|
|
5562
|
+
return {
|
|
5563
|
+
exitCode: code,
|
|
5564
|
+
...present
|
|
5565
|
+
};
|
|
5566
|
+
}));
|
|
5567
|
+
const bashHandler = (params) => Effect.gen(function* () {
|
|
5568
|
+
const shell = yield* ToolShell;
|
|
5569
|
+
return shell.stream !== void 0 ? yield* runStreaming(shell.stream, params) : yield* runBuffered(shell, params);
|
|
5598
5570
|
});
|
|
5599
|
-
/**
|
|
5600
|
-
const
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5571
|
+
/** The bash tool: definition + handler, wired the testable (def/exec split) way. */
|
|
5572
|
+
const bashTool = tool_exports.implement(bashDef, bashHandler);
|
|
5573
|
+
const bashPlugin = (0, plugin_exports$1.define)({
|
|
5574
|
+
id: "codework.tool.bash",
|
|
5575
|
+
kind: "tool",
|
|
5576
|
+
setup: Effect.fn("BashPlugin.setup")(function* (ctx) {
|
|
5577
|
+
const shell = yield* io_exports.SandboxIO.Shell;
|
|
5578
|
+
const mounted = fromSandboxShell.pipe(Layer.provide(Layer.succeed(io_exports.SandboxIO.Shell, shell)));
|
|
5579
|
+
ctx.plugin.tools.add(tool_exports.provide(bashTool, mounted));
|
|
5580
|
+
})
|
|
5604
5581
|
});
|
|
5605
5582
|
//#endregion
|
|
5606
|
-
//#region src/plugin/
|
|
5607
|
-
/** How long to wait for another holder before giving up. */
|
|
5608
|
-
const TIMEOUT = Duration.minutes(2);
|
|
5609
|
-
/** The holder refreshes the lock's mtime this often; one not refreshed for the timeout is stale. */
|
|
5610
|
-
const HEARTBEAT = Duration.seconds(15);
|
|
5611
|
-
const abandoned = (directory) => Effect.gen(function* () {
|
|
5612
|
-
const info = yield* fileSystem.stat(directory);
|
|
5613
|
-
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
5614
|
-
return Option.exists(info.mtime, (mtime) => now - mtime.getTime() > Duration.toMillis(TIMEOUT));
|
|
5615
|
-
}).pipe(Effect.orElseSucceed(() => false));
|
|
5616
|
-
const heartbeat = (directory) => Effect.clockWith((clock) => clock.currentTimeMillis).pipe(Effect.flatMap((now) => fileSystem.utimes(directory, now, now)), Effect.ignore, Effect.repeat(Schedule.spaced(HEARTBEAT)));
|
|
5583
|
+
//#region src/plugin/builtin.ts
|
|
5617
5584
|
/**
|
|
5618
|
-
*
|
|
5619
|
-
* the
|
|
5620
|
-
*
|
|
5621
|
-
* One finalizer is registered for the whole wait rather than one per 50ms poll, and a single
|
|
5622
|
-
* attempt is `uninterruptible` so `mkdir` and recording ownership cannot be split -- an interrupt
|
|
5623
|
-
* between them would leave behind a lock the finalizer does not know to remove.
|
|
5624
|
-
*
|
|
5625
|
-
* `onTimeout` names the failure, because what a caller is waiting for is the caller's to describe.
|
|
5585
|
+
* Every plugin the harness ships, and the selection a caller who passes no `plugins` gets. Tool
|
|
5586
|
+
* contributors come first so the Prompt plugin that indexes them runs after they registered.
|
|
5626
5587
|
*/
|
|
5627
|
-
const
|
|
5628
|
-
const held = yield* Ref.make(false);
|
|
5629
|
-
yield* Effect.acquireRelease(Effect.void, () => Ref.get(held).pipe(Effect.flatMap((owned) => owned ? fileSystem.remove(directory, {
|
|
5630
|
-
recursive: true,
|
|
5631
|
-
force: true
|
|
5632
|
-
}) : Effect.void), Effect.orDie));
|
|
5633
|
-
if (!(yield* fileSystem.makeDirectory(directory).pipe(Effect.andThen(Ref.set(held, true)), Effect.as(true), Effect.catchIf((error) => error.reason._tag === "AlreadyExists", () => abandoned(directory).pipe(Effect.flatMap((stale) => stale ? fileSystem.remove(directory, {
|
|
5634
|
-
recursive: true,
|
|
5635
|
-
force: true
|
|
5636
|
-
}).pipe(Effect.as(false)) : Effect.succeed(false)))), Effect.uninterruptible).pipe(Effect.repeat({
|
|
5637
|
-
schedule: Schedule.spaced("50 millis"),
|
|
5638
|
-
until: (owned) => owned
|
|
5639
|
-
}), Effect.timeoutOrElse({
|
|
5640
|
-
duration: TIMEOUT,
|
|
5641
|
-
orElse: () => Effect.succeed(false)
|
|
5642
|
-
})))) return yield* Effect.fail(onTimeout());
|
|
5643
|
-
yield* Effect.forkScoped(heartbeat(directory));
|
|
5644
|
-
});
|
|
5588
|
+
const builtins = Object.freeze([bashPlugin, defaultPromptPlugin]);
|
|
5645
5589
|
//#endregion
|
|
5646
|
-
//#region src/
|
|
5647
|
-
/** Facts about the *package*, and nothing about the plugin inside it. */
|
|
5648
|
-
const Marker = Schema.Struct({
|
|
5649
|
-
spec: Schema.String,
|
|
5650
|
-
name: Schema.String,
|
|
5651
|
-
version: Schema.optional(Schema.String),
|
|
5652
|
-
revision: Schema.optional(Schema.String),
|
|
5653
|
-
/** Relative to the generation directory, so the marker survives the publishing rename. */
|
|
5654
|
-
entrypoint: Schema.String,
|
|
5655
|
-
createdAt: Schema.Finite
|
|
5656
|
-
});
|
|
5657
|
-
const MARKER = ".complete.json";
|
|
5658
|
-
const STAGING = ".staging-";
|
|
5659
|
-
const root = (cache) => hostPath.join(cache, "plugins", "v2");
|
|
5590
|
+
//#region src/settings/merge.ts
|
|
5660
5591
|
/**
|
|
5661
|
-
* A
|
|
5662
|
-
*
|
|
5663
|
-
*
|
|
5592
|
+
* A plain string-keyed record: what a settings patch merges, and what a plugin's `options` block
|
|
5593
|
+
* has to be. A Map, a typed array or a class instance is an object to `typeof` and not one of
|
|
5594
|
+
* these — `Object.freeze` alone does not tell them apart, and it throws on some of them.
|
|
5664
5595
|
*/
|
|
5665
|
-
const
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5596
|
+
const isRecord$1 = (value) => Predicate.isObject(value) && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
5597
|
+
const copy = (value) => {
|
|
5598
|
+
if (Array.isArray(value)) return value.map(copy);
|
|
5599
|
+
return isRecord$1(value) ? merge({}, value) : value;
|
|
5600
|
+
};
|
|
5601
|
+
/** Null/undefined properties are absent, objects merge, and arrays replace. Never mutates inputs. */
|
|
5602
|
+
function merge(base, ...patches) {
|
|
5603
|
+
const result = /* @__PURE__ */ new Map();
|
|
5604
|
+
for (const patch of [base, ...patches]) {
|
|
5605
|
+
if (patch === void 0) continue;
|
|
5606
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
5607
|
+
if (value === null || value === void 0) continue;
|
|
5608
|
+
const previous = result.get(key);
|
|
5609
|
+
result.set(key, isRecord$1(previous) && isRecord$1(value) ? merge(previous, value) : copy(value));
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
5612
|
+
return Object.fromEntries(result);
|
|
5613
|
+
}
|
|
5614
|
+
/** Normalize nullable properties before schema validation, without changing array positions. */
|
|
5615
|
+
const normalize = copy;
|
|
5616
|
+
//#endregion
|
|
5617
|
+
//#region src/util/module.ts
|
|
5618
|
+
/*!
|
|
5619
|
+
* Adapted from OpenCode packages/util/src/runtime/import.node.ts.
|
|
5669
5620
|
*
|
|
5670
|
-
*
|
|
5671
|
-
*
|
|
5672
|
-
*
|
|
5621
|
+
* MIT License
|
|
5622
|
+
*
|
|
5623
|
+
* Copyright (c) 2025 opencode
|
|
5624
|
+
*
|
|
5625
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5626
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
5627
|
+
* in the Software without restriction, including without limitation the rights
|
|
5628
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
5629
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
5630
|
+
* furnished to do so, subject to the following conditions:
|
|
5631
|
+
*
|
|
5632
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
5633
|
+
* copies or substantial portions of the Software.
|
|
5634
|
+
*
|
|
5635
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
5636
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
5637
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
5638
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
5639
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
5640
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
5641
|
+
* SOFTWARE.
|
|
5673
5642
|
*/
|
|
5674
|
-
const digest = Effect.fn("PluginStore.digest")(function* (target, cache, from) {
|
|
5675
|
-
const source = target.kind === "registry" ? `${yield* registry(target, cache, from)}\0${target.spec}` : target.spec;
|
|
5676
|
-
return Encoding.encodeHex(yield* crypto.digest("SHA-256", new TextEncoder().encode(source)));
|
|
5677
|
-
});
|
|
5678
|
-
const entryDir = Effect.fn("PluginStore.entryDir")(function* (target, cache, from) {
|
|
5679
|
-
const key = yield* digest(target, cache, from);
|
|
5680
|
-
return {
|
|
5681
|
-
key,
|
|
5682
|
-
directory: hostPath.join(root(cache), slugOf(target), key)
|
|
5683
|
-
};
|
|
5684
|
-
});
|
|
5685
|
-
const failure$1 = (target, reason, message, cause) => new StoreError({
|
|
5686
|
-
reason,
|
|
5687
|
-
reference: target.spec,
|
|
5688
|
-
message,
|
|
5689
|
-
...cause === void 0 ? {} : { cause }
|
|
5690
|
-
});
|
|
5691
|
-
/** Generation directories, oldest first. A name that is not a number is not a generation. */
|
|
5692
|
-
const generations = Effect.fn("PluginStore.generations")(function* (directory) {
|
|
5693
|
-
return (yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []))).filter((name) => /^\d+$/.test(name)).map((name) => Number(name)).sort((left, right) => left - right);
|
|
5694
|
-
});
|
|
5695
|
-
const readMarker = (directory) => fileSystem.readFileString(hostPath.join(directory, MARKER)).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Marker))), Effect.option);
|
|
5696
|
-
const entryOf = (marker, input) => ({
|
|
5697
|
-
...marker,
|
|
5698
|
-
digest: input.digest,
|
|
5699
|
-
generation: input.generation,
|
|
5700
|
-
directory: input.directory,
|
|
5701
|
-
url: url(hostPath.resolve(input.directory, marker.entrypoint))
|
|
5702
|
-
});
|
|
5703
|
-
/** The newest generation that carries a marker. Unmarked ones are invisible: they never happened. */
|
|
5704
|
-
const active = Effect.fn("PluginStore.active")(function* (directory, key) {
|
|
5705
|
-
for (const generation of (yield* generations(directory)).reverse()) {
|
|
5706
|
-
const where = hostPath.join(directory, String(generation));
|
|
5707
|
-
const marker = yield* readMarker(where);
|
|
5708
|
-
if (Option.isSome(marker)) return entryOf(marker.value, {
|
|
5709
|
-
digest: key,
|
|
5710
|
-
generation,
|
|
5711
|
-
directory: where
|
|
5712
|
-
});
|
|
5713
|
-
}
|
|
5714
|
-
});
|
|
5715
|
-
/** What the index files about an entry, all of it derivable from the spec and the marker. */
|
|
5716
|
-
const record = (target, entry, installedAt) => ({
|
|
5717
|
-
spec: target.spec,
|
|
5718
|
-
source: target.kind,
|
|
5719
|
-
name: entry.name,
|
|
5720
|
-
slug: slugOf(target),
|
|
5721
|
-
mutable: target.mutable,
|
|
5722
|
-
generation: entry.generation,
|
|
5723
|
-
...entry.version === void 0 ? {} : { version: entry.version },
|
|
5724
|
-
...entry.revision === void 0 ? {} : { revision: entry.revision },
|
|
5725
|
-
entrypoint: entry.entrypoint,
|
|
5726
|
-
installedAt
|
|
5727
|
-
});
|
|
5728
5643
|
/**
|
|
5729
|
-
*
|
|
5730
|
-
* offline works.
|
|
5644
|
+
* Packages a plugin must share with the harness instead of using the copy installed beside it.
|
|
5731
5645
|
*
|
|
5732
|
-
*
|
|
5733
|
-
*
|
|
5646
|
+
* A plugin is installed into the store as its own npm tree, so `effect` and the plugin SDK land
|
|
5647
|
+
* there a second time. Two copies of the *same version* are still two module instances, and
|
|
5648
|
+
* Effect Schema's checks do not survive the crossing: a schema built by one copy and evaluated by
|
|
5649
|
+
* the other reports every check as failed, so a tool whose success type is `Schema.Finite` dies
|
|
5650
|
+
* on "Expected a finite number" for `22800`. Nothing about that is visible at registration --
|
|
5651
|
+
* `Schema.isSchema` passes, the JSON Schema is derived correctly, and the tool reaches the model
|
|
5652
|
+
* -- so it surfaces on the first call rather than at boot.
|
|
5653
|
+
*
|
|
5654
|
+
* Resolution is the right place to fix it. A symlink into the store cannot be: a generation is
|
|
5655
|
+
* immutable and shared by every project on the machine, while the harness doing the loading is
|
|
5656
|
+
* whichever one is running now, and two applications embedding the harness need not agree on
|
|
5657
|
+
* where their `effect` lives.
|
|
5734
5658
|
*/
|
|
5735
|
-
const
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5659
|
+
const sharedPackages = /* @__PURE__ */ new Set(["effect", "@codeworksh/plugin"]);
|
|
5660
|
+
/** The package a bare specifier names: `effect/Schema` -> `effect`, `@a/b/c` -> `@a/b`. */
|
|
5661
|
+
function packageOf(specifier) {
|
|
5662
|
+
if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.includes(":")) return "";
|
|
5663
|
+
const segments = specifier.split("/");
|
|
5664
|
+
return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0] ?? "";
|
|
5665
|
+
}
|
|
5666
|
+
/**
|
|
5667
|
+
* Resolve the shared packages as though this module had imported them, for as long as `run`
|
|
5668
|
+
* takes. Anything else resolves exactly as it would have.
|
|
5669
|
+
*/
|
|
5670
|
+
function withSharedModules(run) {
|
|
5671
|
+
const hook = registerHooks({ resolve(specifier, context, nextResolve) {
|
|
5672
|
+
return sharedPackages.has(packageOf(specifier)) ? nextResolve(specifier, {
|
|
5673
|
+
...context,
|
|
5674
|
+
parentURL: import.meta.url
|
|
5675
|
+
}) : nextResolve(specifier, context);
|
|
5676
|
+
} });
|
|
5677
|
+
return run().finally(() => hook.deregister());
|
|
5678
|
+
}
|
|
5679
|
+
/** Use Node's main loader even when the caller runs inside a bundler or test VM. */
|
|
5680
|
+
async function importModule(specifier) {
|
|
5681
|
+
const imported = await withSharedModules(() => new Script(`import(${JSON.stringify(specifier)})`, { importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER }).runInThisContext());
|
|
5682
|
+
if (typeof imported !== "object" || imported === null) return imported;
|
|
5683
|
+
const module = imported;
|
|
5684
|
+
const exports = module["module.exports"];
|
|
5685
|
+
if (exports !== module.default || typeof exports !== "object" && typeof exports !== "function" || exports === null) return imported;
|
|
5686
|
+
return Object.assign({}, module, exports);
|
|
5687
|
+
}
|
|
5688
|
+
/** Node resolution scoped synchronously to the caller. */
|
|
5689
|
+
function resolveModule(specifier, directory, conditions) {
|
|
5690
|
+
const hook = registerHooks({ resolve(target, context, nextResolve) {
|
|
5691
|
+
return nextResolve(target, {
|
|
5692
|
+
...context,
|
|
5693
|
+
parentURL: pathToFileURL(path.join(directory, "package.json")).href,
|
|
5694
|
+
...conditions === void 0 ? {} : { conditions: [.../* @__PURE__ */ new Set([...context.conditions, ...conditions])] }
|
|
5746
5695
|
});
|
|
5696
|
+
} });
|
|
5697
|
+
try {
|
|
5698
|
+
const resolve = (target) => {
|
|
5699
|
+
const resolved = import.meta.resolve(path.isAbsolute(target) ? pathToFileURL(target).href : target);
|
|
5700
|
+
if (resolved.startsWith("file:")) statSync(new URL(resolved));
|
|
5701
|
+
return resolved;
|
|
5702
|
+
};
|
|
5703
|
+
try {
|
|
5704
|
+
return resolve(specifier);
|
|
5705
|
+
} catch (error) {
|
|
5706
|
+
if (path.extname(specifier) || !missing(error)) throw error;
|
|
5707
|
+
for (const extension of [
|
|
5708
|
+
".ts",
|
|
5709
|
+
".tsx",
|
|
5710
|
+
".js",
|
|
5711
|
+
".jsx",
|
|
5712
|
+
".mts",
|
|
5713
|
+
".mjs",
|
|
5714
|
+
".cts",
|
|
5715
|
+
".cjs"
|
|
5716
|
+
]) try {
|
|
5717
|
+
return resolve(specifier + extension);
|
|
5718
|
+
} catch (cause) {
|
|
5719
|
+
if (!missing(cause)) throw cause;
|
|
5720
|
+
}
|
|
5721
|
+
throw error;
|
|
5722
|
+
}
|
|
5723
|
+
} finally {
|
|
5724
|
+
hook.deregister();
|
|
5747
5725
|
}
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5726
|
+
}
|
|
5727
|
+
function missing(error) {
|
|
5728
|
+
return error instanceof Error && "code" in error && [
|
|
5729
|
+
"ENOENT",
|
|
5730
|
+
"ENOTDIR",
|
|
5731
|
+
"ERR_MODULE_NOT_FOUND"
|
|
5732
|
+
].includes(String(error.code));
|
|
5733
|
+
}
|
|
5734
|
+
//#endregion
|
|
5735
|
+
//#region src/plugin/error.ts
|
|
5758
5736
|
/**
|
|
5759
|
-
*
|
|
5760
|
-
*
|
|
5761
|
-
* `plugin remove` uses this as its installer: it has to identify what an entry names without
|
|
5762
|
-
* fetching anything, and a spec with nothing filed is a real answer there -- the command reports
|
|
5763
|
-
* it and moves on.
|
|
5737
|
+
* The reference as the person wrote it -- the string they will recognise in their settings file,
|
|
5738
|
+
* never the resolved path or the canonical spec. Every plugin failure carries one.
|
|
5764
5739
|
*/
|
|
5765
|
-
const
|
|
5766
|
-
const found = yield* resolve$2(target, cache, from);
|
|
5767
|
-
if (found === void 0) return yield* failure$1(target, "plugin-not-installed", `${target.spec} is not installed`);
|
|
5768
|
-
return found;
|
|
5769
|
-
});
|
|
5740
|
+
const Reference = Schema.String;
|
|
5770
5741
|
/**
|
|
5771
|
-
*
|
|
5742
|
+
* The settings file that declared the entry, when one did.
|
|
5772
5743
|
*
|
|
5773
|
-
*
|
|
5774
|
-
*
|
|
5744
|
+
* Absent for an embedder's own list and for anything the CLI was handed directly -- in both cases
|
|
5745
|
+
* the caller already knows where the string came from. It is the settings path that needs saying,
|
|
5746
|
+
* because several layers accumulate entries and "which file says this" is the question.
|
|
5775
5747
|
*/
|
|
5776
|
-
const
|
|
5777
|
-
return yield* Effect.gen(function* () {
|
|
5778
|
-
const { key, directory } = yield* entryDir(target, cache, options.from);
|
|
5779
|
-
if (options.refresh !== true) {
|
|
5780
|
-
const found = yield* resolve$2(target, cache, options.from);
|
|
5781
|
-
if (found !== void 0) return { entry: found };
|
|
5782
|
-
}
|
|
5783
|
-
yield* fileSystem.makeDirectory(directory, { recursive: true });
|
|
5784
|
-
yield* lock(hostPath.join(directory, ".lock"), () => failure$1(target, "plugin-lock-timeout", `timed out waiting for another installation of ${target.spec}`));
|
|
5785
|
-
const current = yield* active(directory, key);
|
|
5786
|
-
if (current !== void 0 && options.refresh !== true) return { entry: current };
|
|
5787
|
-
const real = yield* fileSystem.realPath(directory).pipe(Effect.orElseSucceed(() => directory));
|
|
5788
|
-
const staging = yield* Effect.acquireRelease(fileSystem.makeTempDirectory({
|
|
5789
|
-
directory: real,
|
|
5790
|
-
prefix: STAGING
|
|
5791
|
-
}), (staged) => fileSystem.remove(staged, {
|
|
5792
|
-
recursive: true,
|
|
5793
|
-
force: true
|
|
5794
|
-
}).pipe(Effect.orDie));
|
|
5795
|
-
yield* fileSystem.writeFileString(hostPath.join(staging, "package.json"), "{\"private\":true,\"type\":\"module\"}");
|
|
5796
|
-
const fetched = yield* download({
|
|
5797
|
-
target,
|
|
5798
|
-
into: staging,
|
|
5799
|
-
cache,
|
|
5800
|
-
from: options.from,
|
|
5801
|
-
...options.runner === void 0 ? {} : { runner: options.runner }
|
|
5802
|
-
});
|
|
5803
|
-
if (current !== void 0 && current.revision === fetched.revision) return { entry: current };
|
|
5804
|
-
const validated = yield* options.validate(fetched);
|
|
5805
|
-
const entrypoint = hostPath.relative(yield* fileSystem.realPath(staging), fetched.entrypoint);
|
|
5806
|
-
if (entrypoint.startsWith("..") || hostPath.isAbsolute(entrypoint)) return yield* new InstallError({
|
|
5807
|
-
reason: "plugin-no-entrypoint",
|
|
5808
|
-
reference: target.spec,
|
|
5809
|
-
message: `entrypoint escapes its installation: ${entrypoint}`
|
|
5810
|
-
});
|
|
5811
|
-
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
5812
|
-
const marker = {
|
|
5813
|
-
spec: target.spec,
|
|
5814
|
-
name: fetched.name,
|
|
5815
|
-
...fetched.version === void 0 ? {} : { version: fetched.version },
|
|
5816
|
-
...fetched.revision === void 0 ? {} : { revision: fetched.revision },
|
|
5817
|
-
entrypoint,
|
|
5818
|
-
createdAt: now
|
|
5819
|
-
};
|
|
5820
|
-
yield* fileSystem.writeFileString(hostPath.join(staging, MARKER), yield* Schema.encodeEffect(Schema.fromJsonString(Marker))(marker));
|
|
5821
|
-
const previous = (yield* generations(directory)).at(-1) ?? 0;
|
|
5822
|
-
const generation = Math.max(now, previous + 1);
|
|
5823
|
-
const published = hostPath.join(directory, String(generation));
|
|
5824
|
-
yield* fileSystem.rename(staging, published);
|
|
5825
|
-
const entry = entryOf(marker, {
|
|
5826
|
-
digest: key,
|
|
5827
|
-
generation,
|
|
5828
|
-
directory: published
|
|
5829
|
-
});
|
|
5830
|
-
yield* put(root(cache), key, record(target, entry, now));
|
|
5831
|
-
yield* collect$1(directory);
|
|
5832
|
-
return {
|
|
5833
|
-
entry,
|
|
5834
|
-
validated
|
|
5835
|
-
};
|
|
5836
|
-
}).pipe(Effect.scoped, Effect.mapError((cause) => Schema.is(InstallError)(cause) || Schema.is(StoreError)(cause) || Schema.is(LoadError)(cause) ? cause : failure$1(target, "plugin-marker-invalid", `cannot write the store entry for ${target.spec}`, cause)));
|
|
5837
|
-
});
|
|
5748
|
+
const File$1 = Schema.optional(Schema.String);
|
|
5838
5749
|
/**
|
|
5839
|
-
*
|
|
5750
|
+
* A spec that cannot be installed at all.
|
|
5840
5751
|
*
|
|
5841
|
-
*
|
|
5842
|
-
*
|
|
5843
|
-
*
|
|
5752
|
+
* `plugin-not-found` is a local path with nothing at it; `plugin-not-installed` (the store's) is a
|
|
5753
|
+
* package spec with no entry. The first is a mistake in a settings file, the second is fixed by
|
|
5754
|
+
* `plugin install`, and keeping them apart is most of the value of having slugs.
|
|
5844
5755
|
*/
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
});
|
|
5869
|
-
const HOUR = 36e5;
|
|
5870
|
-
const WEEK = 168 * HOUR;
|
|
5756
|
+
var SourceError = class extends Schema.TaggedError()("PluginSourceError", {
|
|
5757
|
+
reason: Schema.Literals([
|
|
5758
|
+
"plugin-unsupported-source",
|
|
5759
|
+
"plugin-not-found",
|
|
5760
|
+
"plugin-escapes-root"
|
|
5761
|
+
]),
|
|
5762
|
+
reference: Reference,
|
|
5763
|
+
file: File$1,
|
|
5764
|
+
message: Schema.String
|
|
5765
|
+
}) {};
|
|
5766
|
+
/** A spec that is installable, and whose install did not produce a usable package. */
|
|
5767
|
+
var InstallError = class extends Schema.TaggedError()("PluginInstallError", {
|
|
5768
|
+
reason: Schema.Literals([
|
|
5769
|
+
"plugin-resolve-failed",
|
|
5770
|
+
"plugin-fetch-failed",
|
|
5771
|
+
"plugin-no-commit",
|
|
5772
|
+
"plugin-no-entrypoint"
|
|
5773
|
+
]),
|
|
5774
|
+
reference: Reference,
|
|
5775
|
+
file: File$1,
|
|
5776
|
+
message: Schema.String,
|
|
5777
|
+
cause: Schema.optional(Schema.Defect())
|
|
5778
|
+
}) {};
|
|
5871
5779
|
/**
|
|
5872
|
-
*
|
|
5780
|
+
* A module that reached the import and did not come back as a plugin.
|
|
5873
5781
|
*
|
|
5874
|
-
*
|
|
5875
|
-
*
|
|
5782
|
+
* Distinct from {@link InstallError}: the bytes are here and are fine as *bytes*. What failed is
|
|
5783
|
+
* evaluating them, or what they turned out to export -- which is the distinction a caller acts on,
|
|
5784
|
+
* because one is fixed by installing and the other by editing code.
|
|
5876
5785
|
*/
|
|
5877
|
-
|
|
5786
|
+
var LoadError = class extends Schema.TaggedError()("PluginLoadError", {
|
|
5787
|
+
reason: Schema.Literals([
|
|
5788
|
+
"plugin-import-failed",
|
|
5789
|
+
"plugin-missing-dependency",
|
|
5790
|
+
"plugin-not-compiled",
|
|
5791
|
+
"plugin-invalid-definition"
|
|
5792
|
+
]),
|
|
5793
|
+
reference: Reference,
|
|
5794
|
+
file: File$1,
|
|
5795
|
+
message: Schema.String,
|
|
5796
|
+
/** The ID the module declared, when it got far enough to declare one. */
|
|
5797
|
+
id: Schema.optional(Schema.String),
|
|
5798
|
+
cause: Schema.optional(Schema.Defect())
|
|
5799
|
+
}) {};
|
|
5800
|
+
/** The store's own failures: what is filed, what is not, and what could not be claimed. */
|
|
5801
|
+
var StoreError = class extends Schema.TaggedError()("PluginStoreError", {
|
|
5802
|
+
reason: Schema.Literals([
|
|
5803
|
+
"plugin-not-installed",
|
|
5804
|
+
"plugin-lock-timeout",
|
|
5805
|
+
"plugin-marker-invalid",
|
|
5806
|
+
"plugin-index-invalid",
|
|
5807
|
+
"plugin-collect-failed"
|
|
5808
|
+
]),
|
|
5809
|
+
reference: Reference,
|
|
5810
|
+
file: File$1,
|
|
5811
|
+
message: Schema.String,
|
|
5812
|
+
cause: Schema.optional(Schema.Defect())
|
|
5813
|
+
}) {};
|
|
5878
5814
|
/**
|
|
5879
|
-
*
|
|
5815
|
+
* Note which settings file declared the entry a failure came from.
|
|
5880
5816
|
*
|
|
5881
|
-
* The
|
|
5882
|
-
*
|
|
5883
|
-
*
|
|
5817
|
+
* The store and the installer raise their own failures without knowing that, because neither is
|
|
5818
|
+
* told: they are handed a spec, not a settings layer. The loader knows, and this is where it says
|
|
5819
|
+
* so -- leaving an existing answer alone, since a more specific one is already better.
|
|
5884
5820
|
*/
|
|
5885
|
-
const
|
|
5886
|
-
if (
|
|
5887
|
-
const
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
const record = (yield* read(base)).get(key);
|
|
5892
|
-
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
5893
|
-
if (options.refresh !== true && record?.checkedAt !== void 0 && now - record.checkedAt < CHECK_TTL && (record.outdated !== true || record.available !== void 0)) return record.outdated === true ? {
|
|
5894
|
-
_tag: "outdated",
|
|
5895
|
-
available: record.available,
|
|
5896
|
-
...entry.revision === void 0 ? {} : { filed: entry.revision }
|
|
5897
|
-
} : {
|
|
5898
|
-
_tag: "current",
|
|
5899
|
-
...entry.revision === void 0 ? {} : { revision: entry.revision }
|
|
5900
|
-
};
|
|
5901
|
-
const available = yield* options.probe(target);
|
|
5902
|
-
if (available === void 0) return {
|
|
5903
|
-
_tag: "current",
|
|
5904
|
-
...entry.revision === void 0 ? {} : { revision: entry.revision }
|
|
5821
|
+
const declaredIn = (error, file) => {
|
|
5822
|
+
if (error.file !== void 0) return error;
|
|
5823
|
+
const shared = {
|
|
5824
|
+
reference: error.reference,
|
|
5825
|
+
message: error.message,
|
|
5826
|
+
file
|
|
5905
5827
|
};
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
outdated,
|
|
5910
|
-
available
|
|
5828
|
+
if (Schema.is(SourceError)(error)) return new SourceError({
|
|
5829
|
+
...shared,
|
|
5830
|
+
reason: error.reason
|
|
5911
5831
|
});
|
|
5912
|
-
return
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
}
|
|
5917
|
-
|
|
5918
|
-
...
|
|
5919
|
-
|
|
5920
|
-
|
|
5832
|
+
if (Schema.is(InstallError)(error)) return new InstallError({
|
|
5833
|
+
...shared,
|
|
5834
|
+
reason: error.reason,
|
|
5835
|
+
cause: error.cause
|
|
5836
|
+
});
|
|
5837
|
+
if (Schema.is(LoadError)(error)) return new LoadError({
|
|
5838
|
+
...shared,
|
|
5839
|
+
reason: error.reason,
|
|
5840
|
+
id: error.id,
|
|
5841
|
+
cause: error.cause
|
|
5842
|
+
});
|
|
5843
|
+
if (Schema.is(StoreError)(error)) return new StoreError({
|
|
5844
|
+
...shared,
|
|
5845
|
+
reason: error.reason,
|
|
5846
|
+
cause: error.cause
|
|
5847
|
+
});
|
|
5848
|
+
return error;
|
|
5849
|
+
};
|
|
5850
|
+
//#endregion
|
|
5851
|
+
//#region src/util/path.ts
|
|
5921
5852
|
/**
|
|
5922
|
-
*
|
|
5853
|
+
* A directory that crosses into the harness is absolute, and a relative one is a **defect** rather
|
|
5854
|
+
* than an error.
|
|
5923
5855
|
*
|
|
5924
|
-
*
|
|
5925
|
-
*
|
|
5856
|
+
* A relative path is a disguised `process.cwd()` call: and every consumer
|
|
5857
|
+
* resolves it silently. `path.resolve("x")` anchors to the process directory, npm answers a
|
|
5858
|
+
* relative `cwd` with a `localPrefix` of whatever repository the process is inside (measured, not
|
|
5859
|
+
* assumed), and `Session.link` would write the *server's* directory into a field whose whole
|
|
5860
|
+
* purpose is to be a host path the client declared.
|
|
5861
|
+
*
|
|
5862
|
+
* For a long-running server the process directory is nobody's project, so the result is a
|
|
5863
|
+
* plausible answer to a question no one asked.
|
|
5864
|
+
* An absolute path is the one spelling none of them can quietly reinterpret.
|
|
5865
|
+
*
|
|
5866
|
+
* A defect rather than a typed error because nothing a user types reaches here. Every reference
|
|
5867
|
+
* goes through `parse`, and every directory comes from `Global.resolve`, a resolved `cwd` or a
|
|
5868
|
+
* session's `hostDir`, all of them already absolute. A relative one means a new call site got it
|
|
5869
|
+
* wrong, and a stack trace names that call site; a typed error would ask every caller to handle a
|
|
5870
|
+
* case none of them can cause.
|
|
5926
5871
|
*/
|
|
5927
|
-
const
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5872
|
+
const rooted = (directory, role) => {
|
|
5873
|
+
if (hostPath.isAbsolute(directory)) return directory;
|
|
5874
|
+
throw new Error(`${role} must be an absolute path, got ${JSON.stringify(directory)}`);
|
|
5875
|
+
};
|
|
5876
|
+
//#endregion
|
|
5877
|
+
//#region src/plugin/npm.ts
|
|
5878
|
+
const Manifest$1 = Schema.Struct({ version: Schema.optional(Schema.String) });
|
|
5879
|
+
const Lockfile = Schema.Struct({ packages: Schema.optional(Schema.Record(Schema.String, Schema.Struct({ resolved: Schema.optional(Schema.String) }))) });
|
|
5880
|
+
/**
|
|
5881
|
+
* npm's flattened config: registry, scopes, auth, and cache paths.
|
|
5882
|
+
*
|
|
5883
|
+
* Built in one place, because two call sites deriving it separately is how the cache location
|
|
5884
|
+
* drifts between an install and the staleness check that is supposed to describe it.
|
|
5885
|
+
*
|
|
5886
|
+
* `dir` is where the `.npmrc` chain is read from, and it is the **host directory the caller
|
|
5887
|
+
* belongs to** -- never the staging directory and never the cache. A company repo keeps its
|
|
5888
|
+
* private registry and its scoped token in its own `.npmrc`, so a chain read anywhere else
|
|
5889
|
+
* resolves the plugin against the public registry and fails 404, or worse, installs a public
|
|
5890
|
+
* package of the same name. This is the registry half of auth; the git half is below.
|
|
5891
|
+
*
|
|
5892
|
+
* No `--prefix`. npm pins its local prefix to a `--prefix` on `argv` and stops walking, so
|
|
5893
|
+
* passing one would look up exactly `<dir>/.npmrc` and nothing above it -- wrong for a person
|
|
5894
|
+
* standing in `packages/x` whose token lives at the repository root. Without it npm walks up from
|
|
5895
|
+
* `cwd` to the nearest `package.json` or `node_modules` exactly as `npm` itself would if it were
|
|
5896
|
+
* run there, which is the only rule a user can predict.
|
|
5897
|
+
*
|
|
5898
|
+
* `--cache` is ours and is passed on `argv`, which outranks every `.npmrc`. npm's `flatten`
|
|
5899
|
+
* derives `_cacache`, `_npx` and `_tuf` from that one value, so naming the parent is enough --
|
|
5900
|
+
* and overriding `flat.cache` afterwards would redirect one of the three and leave `_tuf` pointing
|
|
5901
|
+
* at the user's home. `--home` exists to make a run self-contained; anything reached by an ambient
|
|
5902
|
+
* default is a test reading the developer's real machine.
|
|
5903
|
+
*
|
|
5904
|
+
* **Nothing here names an `env`.** npm hands git `opts.env` when one is present and falls back to
|
|
5905
|
+
* `{ ...gitDefaults, ...process.env }` when it is not, so leaving it out is what lets a git source
|
|
5906
|
+
* reach a private remote at all: `SSH_AUTH_SOCK` finds the agent, `HOME` finds the `~/.gitconfig`
|
|
5907
|
+
* that names the credential helper, and `PATH` finds `git`. Setting `env` to anything -- even to
|
|
5908
|
+
* a copy of `process.env` -- would be a standing invitation to prune it later and break private
|
|
5909
|
+
* git silently, months from the change. §15 Q2.
|
|
5910
|
+
*/
|
|
5911
|
+
const options = (dir, cache) => Effect.suspend(() => {
|
|
5912
|
+
const root = rooted(dir, "the directory a plugin install reads its .npmrc chain from");
|
|
5913
|
+
const store = rooted(cache, "the npm cache");
|
|
5914
|
+
return npmOptions(root, store);
|
|
5962
5915
|
});
|
|
5963
|
-
Effect.
|
|
5964
|
-
const
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5916
|
+
const npmOptions = (dir, cache) => Effect.gen(function* () {
|
|
5917
|
+
const config = yield* Effect.tryPromise(async () => {
|
|
5918
|
+
const { default: Config } = await import("@npmcli/config");
|
|
5919
|
+
const { definitions, flatten, nerfDarts, shorthands } = (await import("@npmcli/config/lib/definitions/index.js")).default;
|
|
5920
|
+
const created = new Config({
|
|
5921
|
+
npmPath: fileURLToPath(new URL("..", import.meta.url)),
|
|
5922
|
+
cwd: dir,
|
|
5923
|
+
env: { ...process.env },
|
|
5924
|
+
argv: [
|
|
5925
|
+
process.execPath,
|
|
5926
|
+
process.execPath,
|
|
5927
|
+
"--cache",
|
|
5928
|
+
hostPath.join(cache, "npm")
|
|
5929
|
+
],
|
|
5930
|
+
execPath: process.execPath,
|
|
5931
|
+
platform: process.platform,
|
|
5932
|
+
definitions,
|
|
5933
|
+
flatten,
|
|
5934
|
+
nerfDarts,
|
|
5935
|
+
shorthands,
|
|
5936
|
+
warn: false
|
|
5937
|
+
});
|
|
5938
|
+
await created.load();
|
|
5939
|
+
return created;
|
|
5940
|
+
}).pipe(Effect.catch((cause) => Effect.logWarning(`cannot load npm configuration from ${dir}; npm defaults apply`, cause).pipe(Effect.as(void 0))));
|
|
5941
|
+
if (config === void 0) return { allowGit: "root" };
|
|
5942
|
+
for (const kind of [
|
|
5943
|
+
"project",
|
|
5944
|
+
"user",
|
|
5945
|
+
"global"
|
|
5946
|
+
]) {
|
|
5947
|
+
const loaded = config.data.get(kind);
|
|
5948
|
+
if (loaded?.loadError !== void 0 && loaded.loadError.code !== "ENOENT") yield* Effect.logWarning(`cannot read npm configuration ${loaded.source ?? `for ${kind}`}; npm defaults apply`, loaded.loadError);
|
|
5973
5949
|
}
|
|
5974
|
-
return
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
id: Id,
|
|
5979
|
-
kind: Schema.Literals(Object.keys(plugin_exports$1.domains)),
|
|
5980
|
-
events: Schema.optional(Schema.Array(Schema.declare((value) => Predicate.hasProperty(value, "type") && Predicate.isString(value.type)))),
|
|
5981
|
-
setup: Schema.declare((value) => Predicate.isFunction(value))
|
|
5950
|
+
return {
|
|
5951
|
+
...config.flat,
|
|
5952
|
+
allowGit: "root"
|
|
5953
|
+
};
|
|
5982
5954
|
});
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
const
|
|
5986
|
-
|
|
5987
|
-
const
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5955
|
+
const PUBLIC_REGISTRY = "https://registry.npmjs.org/";
|
|
5956
|
+
/** Stable, credential-free registry identity used to address registry artifacts in the store. */
|
|
5957
|
+
const registry = Effect.fn("PluginNpm.registry")(function* (target, cache, from) {
|
|
5958
|
+
const flat = yield* options(from, cache);
|
|
5959
|
+
const scope = target.name.startsWith("@") ? target.name.slice(0, target.name.indexOf("/")) : void 0;
|
|
5960
|
+
const configured = (scope === void 0 ? void 0 : flat[`${scope}:registry`]) ?? flat.registry ?? PUBLIC_REGISTRY;
|
|
5961
|
+
if (typeof configured !== "string") return yield* new InstallError({
|
|
5962
|
+
reason: "plugin-resolve-failed",
|
|
5963
|
+
reference: target.spec,
|
|
5964
|
+
message: `npm registry for ${target.name} is not a URL`
|
|
5965
|
+
});
|
|
5966
|
+
return yield* Effect.try({
|
|
5967
|
+
try: () => {
|
|
5968
|
+
const value = new URL(configured);
|
|
5969
|
+
value.username = "";
|
|
5970
|
+
value.password = "";
|
|
5971
|
+
value.hash = "";
|
|
5972
|
+
return value.href;
|
|
5973
|
+
},
|
|
5974
|
+
catch: (cause) => new InstallError({
|
|
5975
|
+
reason: "plugin-resolve-failed",
|
|
5976
|
+
reference: target.spec,
|
|
5977
|
+
message: `npm registry for ${target.name} is not a valid URL: ${configured}`,
|
|
5978
|
+
cause
|
|
5979
|
+
})
|
|
5980
|
+
});
|
|
5992
5981
|
});
|
|
5993
|
-
/**
|
|
5994
|
-
const
|
|
5982
|
+
/** A 40- or 64-hex commit out of the `resolved` URL arborist writes into the lockfile. */
|
|
5983
|
+
const commitIn = (resolved) => resolved?.match(/#([a-f0-9]{40}|[a-f0-9]{64})(?=::|$)/i)?.[1];
|
|
5995
5984
|
/**
|
|
5996
|
-
*
|
|
5997
|
-
*
|
|
5998
|
-
* Two of these get their own reason, because the remedy differs and that is the whole value of a
|
|
5999
|
-
* slug. A missing dependency means the plugin is fine and its `node_modules` is not, which a
|
|
6000
|
-
* person fixes in the package rather than in their settings. An uncompiled one means the plugin
|
|
6001
|
-
* shipped its sources: Node refuses to strip types under `node_modules`, and installing a plugin
|
|
6002
|
-
* runs no lifecycle scripts, so a `prepare` build never happens either -- the author has to ship
|
|
6003
|
-
* the build output, and nothing the user can do to their settings will help.
|
|
5985
|
+
* The commit a git install actually landed on, read back from the lockfile arborist wrote.
|
|
6004
5986
|
*
|
|
6005
|
-
*
|
|
6006
|
-
* Node's documented identifier and exact, where the wording is free to change under us. Only an
|
|
6007
|
-
* installed plugin can reach it -- a local path lives outside `node_modules`, where Node strips
|
|
6008
|
-
* types happily, which is what keeps the single-file dev workflow working.
|
|
5987
|
+
* Both spellings are tried because which one exists depends on whether the tree was saved.
|
|
6009
5988
|
*/
|
|
6010
|
-
const
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
});
|
|
6017
|
-
/** A module that imported and is not a plugin. */
|
|
6018
|
-
const definitionFailure = (origin, cause, id) => new LoadError({
|
|
6019
|
-
reason: "plugin-invalid-definition",
|
|
6020
|
-
reference: origin.reference,
|
|
6021
|
-
...where(origin),
|
|
6022
|
-
message: detail(cause),
|
|
6023
|
-
...id === void 0 ? {} : { id },
|
|
6024
|
-
cause
|
|
6025
|
-
});
|
|
6026
|
-
const validate = Effect.fn("PluginLoader.validate")(function* (input, origin, builtin = false) {
|
|
6027
|
-
yield* Schema.decodeUnknownEffect(Definition)(input).pipe(Effect.mapError((cause) => definitionFailure(origin, cause)));
|
|
6028
|
-
const plugin = input;
|
|
6029
|
-
if (!builtin && plugin.id.startsWith("codework.")) return yield* definitionFailure(origin, /* @__PURE__ */ new Error("codework namespace is reserved for builtins"), plugin.id);
|
|
6030
|
-
return plugin;
|
|
6031
|
-
});
|
|
6032
|
-
const Manifest = Schema.Struct({
|
|
6033
|
-
name: Schema.optional(Schema.String),
|
|
6034
|
-
exports: Schema.optional(Schema.Unknown)
|
|
6035
|
-
});
|
|
6036
|
-
const localUrl = Effect.fn("PluginLoader.localUrl")(function* (location, origin) {
|
|
6037
|
-
const escapes = (message) => new SourceError({
|
|
6038
|
-
reason: "plugin-escapes-root",
|
|
6039
|
-
reference: origin.reference,
|
|
6040
|
-
...where(origin),
|
|
6041
|
-
message
|
|
6042
|
-
});
|
|
6043
|
-
if ((yield* fileSystem.stat(location)).type !== "Directory") return { url: yield* Effect.try(() => resolveModule(location, hostPath.dirname(location))) };
|
|
6044
|
-
const manifestPath = hostPath.join(location, "package.json");
|
|
6045
|
-
if (!(yield* fileSystem.exists(manifestPath))) return { url: yield* Effect.try(() => resolveModule("./index", location)) };
|
|
6046
|
-
const manifest = yield* fileSystem.readFileString(manifestPath).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest))));
|
|
6047
|
-
if (manifest.exports !== void 0) {
|
|
6048
|
-
const name = manifest.name;
|
|
6049
|
-
if (!name) return yield* escapes("a local package with exports must declare its name");
|
|
6050
|
-
const url = yield* Effect.try(() => resolveModule(name, location));
|
|
6051
|
-
const resolved = yield* fileSystem.realPath(fileURLToPath(url));
|
|
6052
|
-
if (hostPath.relative(yield* fileSystem.realPath(location), resolved).startsWith("..")) return yield* escapes("package export escapes its root");
|
|
6053
|
-
return {
|
|
6054
|
-
url: pathToFileURL(resolved).href,
|
|
6055
|
-
name
|
|
6056
|
-
};
|
|
5989
|
+
const commit = Effect.fn("PluginNpm.commit")(function* (root, name) {
|
|
5990
|
+
for (const file of [hostPath.join(root, "package-lock.json"), hostPath.join(root, "node_modules", ".package-lock.json")]) {
|
|
5991
|
+
const parsed = yield* fileSystem.readFileString(file).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Lockfile))), Effect.option);
|
|
5992
|
+
if (Option.isNone(parsed)) continue;
|
|
5993
|
+
const found = commitIn(parsed.value.packages?.[`node_modules/${name}`]?.resolved);
|
|
5994
|
+
if (found !== void 0) return found;
|
|
6057
5995
|
}
|
|
6058
|
-
const url = yield* Effect.try(() => resolveModule(createRequire(pathToFileURL(manifestPath)).resolve(location), location));
|
|
6059
|
-
return manifest.name === void 0 ? { url } : {
|
|
6060
|
-
url,
|
|
6061
|
-
name: manifest.name
|
|
6062
|
-
};
|
|
6063
5996
|
});
|
|
5997
|
+
/** Node error codes that mean "nothing here", as opposed to "this package is broken". */
|
|
5998
|
+
const notFound = /* @__PURE__ */ new Set([
|
|
5999
|
+
"ENOENT",
|
|
6000
|
+
"ENOTDIR",
|
|
6001
|
+
"MODULE_NOT_FOUND",
|
|
6002
|
+
"ERR_MODULE_NOT_FOUND",
|
|
6003
|
+
"ERR_PACKAGE_PATH_NOT_EXPORTED",
|
|
6004
|
+
"ERR_UNSUPPORTED_DIR_IMPORT"
|
|
6005
|
+
]);
|
|
6064
6006
|
/**
|
|
6065
|
-
*
|
|
6007
|
+
* The module to import out of an installed package.
|
|
6066
6008
|
*
|
|
6067
|
-
*
|
|
6068
|
-
*
|
|
6069
|
-
*
|
|
6070
|
-
*
|
|
6009
|
+
* Ordered subpaths through the package's own `exports`, so a package that publishes a dedicated
|
|
6010
|
+
* `./plugin` entry is honoured and one that does not falls back to its main.
|
|
6011
|
+
*
|
|
6012
|
+
* Only "not found" is tolerated. Swallowing every error would turn a package that is broken --
|
|
6013
|
+
* a syntax error in its manifest, a permission problem -- into "no entrypoint", which is the
|
|
6014
|
+
* wrong diagnosis in the one place a person needs the right one.
|
|
6071
6015
|
*/
|
|
6072
|
-
const
|
|
6073
|
-
const
|
|
6074
|
-
|
|
6075
|
-
|
|
6016
|
+
const entrypoint = (directory, name) => Effect.gen(function* () {
|
|
6017
|
+
for (const subpath of ["", "plugin"]) {
|
|
6018
|
+
const specifier = name === void 0 ? hostPath.join(directory, subpath === "" ? "index" : subpath) : hostPath.join(name, subpath);
|
|
6019
|
+
const resolved = yield* Effect.try(() => resolveModule(specifier, directory)).pipe(Effect.asSome, Effect.catchIf((error) => notFound.has(String(error.cause?.code)), () => Effect.succeed(Option.none())), Effect.mapError((error) => new InstallError({
|
|
6020
|
+
reason: "plugin-no-entrypoint",
|
|
6021
|
+
reference: name ?? directory,
|
|
6022
|
+
message: `cannot resolve an entrypoint in ${directory}`,
|
|
6023
|
+
cause: error
|
|
6024
|
+
})));
|
|
6025
|
+
if (Option.isSome(resolved)) return fileURLToPath(resolved.value);
|
|
6026
|
+
}
|
|
6027
|
+
return yield* new InstallError({
|
|
6028
|
+
reason: "plugin-no-entrypoint",
|
|
6029
|
+
reference: name ?? directory,
|
|
6030
|
+
message: `no plugin entrypoint in ${directory}`
|
|
6076
6031
|
});
|
|
6077
|
-
return {
|
|
6078
|
-
url: added.entry.url,
|
|
6079
|
-
...added.entry.version === void 0 ? {} : { version: added.entry.version },
|
|
6080
|
-
generation: added.entry.generation,
|
|
6081
|
-
...added.validated === void 0 ? {} : { plugin: added.validated }
|
|
6082
|
-
};
|
|
6083
6032
|
});
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
};
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6033
|
+
const reify = Effect.fn("PluginNpm.reify")(function* ({ target, into, cache, from }) {
|
|
6034
|
+
const flat = yield* options(from, cache);
|
|
6035
|
+
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"));
|
|
6036
|
+
const settings = {
|
|
6037
|
+
...flat,
|
|
6038
|
+
...target.mutable ? {
|
|
6039
|
+
preferOnline: true,
|
|
6040
|
+
noGitRevCache: true
|
|
6041
|
+
} : {},
|
|
6042
|
+
audit: false
|
|
6043
|
+
};
|
|
6044
|
+
const arborist = new Arborist({
|
|
6045
|
+
...settings,
|
|
6046
|
+
path: into,
|
|
6047
|
+
binLinks: true,
|
|
6048
|
+
progress: false,
|
|
6049
|
+
savePrefix: "",
|
|
6050
|
+
ignoreScripts: true
|
|
6051
|
+
});
|
|
6052
|
+
return yield* Effect.tryPromise({
|
|
6053
|
+
try: () => arborist.reify({
|
|
6054
|
+
...settings,
|
|
6055
|
+
add: [target.spec],
|
|
6056
|
+
update: target.mutable,
|
|
6057
|
+
save: true,
|
|
6058
|
+
saveType: "prod"
|
|
6059
|
+
}),
|
|
6060
|
+
catch: (cause) => new InstallError({
|
|
6061
|
+
reason: "plugin-fetch-failed",
|
|
6062
|
+
reference: target.spec,
|
|
6063
|
+
message: `cannot install ${target.spec}`,
|
|
6064
|
+
cause
|
|
6065
|
+
})
|
|
6102
6066
|
});
|
|
6103
|
-
return yield* validate(Predicate.isObject(module) && "default" in module ? module.default : void 0, origin);
|
|
6104
6067
|
});
|
|
6105
6068
|
/**
|
|
6106
|
-
*
|
|
6069
|
+
* Fill `into` with the package `target` names, and report what landed there.
|
|
6107
6070
|
*
|
|
6108
|
-
*
|
|
6109
|
-
*
|
|
6110
|
-
*/
|
|
6111
|
-
const definition$1 = (entrypoint, reference) => imported(url(entrypoint), {
|
|
6112
|
-
index: 0,
|
|
6113
|
-
reference
|
|
6114
|
-
});
|
|
6115
|
-
/**
|
|
6116
|
-
* Install and import one reference, and report what it turned out to be, without selecting it.
|
|
6117
|
-
* `codework plugin add` uses this to prove a package really is a plugin before it writes the
|
|
6118
|
-
* reference into a settings file: the failure a user gets is the one they can act on — a bad
|
|
6119
|
-
* spec, an install that did not resolve, a module that exports no plugin — rather than a broken
|
|
6120
|
-
* configuration that only fails at the next run.
|
|
6071
|
+
* A local target downloads nothing: the files on disk are already the answer, and a local plugin
|
|
6072
|
+
* is never copied into the store.
|
|
6121
6073
|
*/
|
|
6122
|
-
const
|
|
6123
|
-
const
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6074
|
+
const download = Effect.fn("PluginNpm.download")(function* (input) {
|
|
6075
|
+
const { target, into, cache, from, runner = reify } = input;
|
|
6076
|
+
if (target.kind === "local") return {
|
|
6077
|
+
directory: target.path,
|
|
6078
|
+
name: hostPath.basename(target.path),
|
|
6079
|
+
entrypoint: yield* entrypoint(target.path)
|
|
6127
6080
|
};
|
|
6128
|
-
const
|
|
6129
|
-
|
|
6081
|
+
const root = yield* fileSystem.realPath(into).pipe(Effect.mapError((cause) => new InstallError({
|
|
6082
|
+
reason: "plugin-fetch-failed",
|
|
6083
|
+
reference: target.spec,
|
|
6084
|
+
message: `cannot resolve the staging directory ${into}`,
|
|
6085
|
+
cause
|
|
6086
|
+
})));
|
|
6087
|
+
const node = (yield* runner({
|
|
6088
|
+
target,
|
|
6089
|
+
into: root,
|
|
6090
|
+
cache,
|
|
6091
|
+
from
|
|
6092
|
+
})).edgesOut.values().next().value?.to;
|
|
6093
|
+
if (node === void 0) return yield* new InstallError({
|
|
6094
|
+
reason: "plugin-fetch-failed",
|
|
6095
|
+
reference: target.spec,
|
|
6096
|
+
message: `${target.spec} installed nothing`
|
|
6097
|
+
});
|
|
6098
|
+
const manifest = yield* fileSystem.readFileString(hostPath.join(node.path, "package.json")).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest$1))), Effect.option);
|
|
6099
|
+
const version = Option.isNone(manifest) ? void 0 : manifest.value.version;
|
|
6100
|
+
const revision = target.kind === "registry" ? version : (yield* commit(root, node.name)) ?? (yield* noCommit(target.spec));
|
|
6130
6101
|
return {
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
...
|
|
6102
|
+
directory: node.path,
|
|
6103
|
+
name: node.name,
|
|
6104
|
+
...version === void 0 ? {} : { version },
|
|
6105
|
+
...revision === void 0 ? {} : { revision },
|
|
6106
|
+
entrypoint: yield* entrypoint(node.path, node.name)
|
|
6134
6107
|
};
|
|
6135
6108
|
});
|
|
6136
|
-
const
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6109
|
+
const noCommit = (spec) => new InstallError({
|
|
6110
|
+
reason: "plugin-no-commit",
|
|
6111
|
+
reference: spec,
|
|
6112
|
+
message: `cannot resolve a commit for ${spec}`
|
|
6113
|
+
});
|
|
6114
|
+
/**
|
|
6115
|
+
* What a mutable target points at right now, without installing it.
|
|
6116
|
+
*
|
|
6117
|
+
* `preferOnline` and `noGitRevCache` are the whole point: without them pacote answers from its own
|
|
6118
|
+
* metadata cache, and a staleness check that reads a cache is not a staleness check. An immutable
|
|
6119
|
+
* target skips the call entirely -- a version and a SHA cannot move.
|
|
6120
|
+
*/
|
|
6121
|
+
const probe = Effect.fn("PluginNpm.probe")(function* (target, cache, from) {
|
|
6122
|
+
if (target.kind === "local" || !target.mutable) return void 0;
|
|
6123
|
+
const opts = {
|
|
6124
|
+
...yield* options(from, cache),
|
|
6125
|
+
preferOnline: true,
|
|
6126
|
+
noGitRevCache: true,
|
|
6127
|
+
_isRoot: true
|
|
6145
6128
|
};
|
|
6129
|
+
const { default: pacote } = yield* Effect.promise(() => import("pacote"));
|
|
6130
|
+
return yield* Effect.tryPromise({
|
|
6131
|
+
try: async () => target.kind === "registry" ? (await pacote.manifest(target.spec, opts)).version : commitIn(await pacote.resolve(target.spec, opts)),
|
|
6132
|
+
catch: (cause) => new InstallError({
|
|
6133
|
+
reason: "plugin-resolve-failed",
|
|
6134
|
+
reference: target.spec,
|
|
6135
|
+
message: `cannot reach ${target.spec}`,
|
|
6136
|
+
cause
|
|
6137
|
+
})
|
|
6138
|
+
});
|
|
6146
6139
|
});
|
|
6147
|
-
/**
|
|
6148
|
-
|
|
6149
|
-
*
|
|
6150
|
-
* Two references naming the same module compare equal through it: `@acme/x@1.2.0` and `@acme/x`
|
|
6151
|
-
* are one package, and `./plugins/x.ts` is the file it resolves to from the directory that
|
|
6152
|
-
* declared it. Pure -- nothing is installed, imported or read.
|
|
6153
|
-
*/
|
|
6154
|
-
const canonical = (reference, hostDir) => Effect.map(parse$2(reference, hostDir), canonical$1);
|
|
6140
|
+
/** A `file:` URL for an entrypoint, which is what the module loader takes. */
|
|
6141
|
+
const url = (entrypoint) => pathToFileURL(entrypoint).href;
|
|
6155
6142
|
//#endregion
|
|
6156
|
-
//#region src/plugin/
|
|
6143
|
+
//#region src/plugin/source.ts
|
|
6144
|
+
/** A committish that names one artifact rather than a moving branch: an unabbreviated SHA. */
|
|
6145
|
+
const isCommit = (committish) => typeof committish === "string" && /^[a-f0-9]{40}$|^[a-f0-9]{64}$/i.test(committish);
|
|
6157
6146
|
/**
|
|
6158
|
-
* A
|
|
6159
|
-
* `
|
|
6160
|
-
*
|
|
6161
|
-
*
|
|
6162
|
-
* e.g
|
|
6163
|
-
* "acme/tool@1.2.0" → module (load me)
|
|
6164
|
-
* define({id, kind, setup, ...}) → module (I AM the definition — embedder code only)
|
|
6165
|
-
* { plugin: "x", enabled: false } → config (don't load — adjust something already loaded)
|
|
6166
|
-
*
|
|
6167
|
-
* setup present → definition, always (even with a stray plugin: key)
|
|
6168
|
-
* setup absent + plugin:/package: present → config patch
|
|
6169
|
-
* setup absent + neither key → malformed
|
|
6147
|
+
* A short, readable name for a git source, used to make a store path recognisable to a human
|
|
6148
|
+
* reading `ls`. It is never an identity: two remotes can slug alike, and the digest is what
|
|
6149
|
+
* separates them.
|
|
6170
6150
|
*/
|
|
6171
|
-
const
|
|
6151
|
+
const slug = (spec) => {
|
|
6152
|
+
const withoutCommittish = spec.split("#")[0] ?? spec;
|
|
6153
|
+
return (() => {
|
|
6154
|
+
try {
|
|
6155
|
+
return decodeURIComponent(withoutCommittish);
|
|
6156
|
+
} catch {
|
|
6157
|
+
return withoutCommittish;
|
|
6158
|
+
}
|
|
6159
|
+
})().replace(/\.git$/i, "").split(/[/:\\]/).at(-1)?.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repository";
|
|
6160
|
+
};
|
|
6161
|
+
/** Whether a reference names a place on disk rather than something to fetch. */
|
|
6162
|
+
const isPath = (reference) => reference.startsWith("file:") || reference.startsWith("./") || reference.startsWith("../") || hostPath.isAbsolute(reference);
|
|
6172
6163
|
/**
|
|
6173
|
-
*
|
|
6174
|
-
*
|
|
6175
|
-
* Runs at boot, at `reload`, and at any exchange that finds a reference it cannot satisfy. It
|
|
6176
|
-
* deliberately knows nothing about `enabled`, options or order -- those are data over modules that
|
|
6177
|
-
* are already loaded, they cost a walk of a short array, and they belong to every exchange.
|
|
6164
|
+
* `from` is the directory a relative spec resolves against, and the caller picks it:
|
|
6178
6165
|
*
|
|
6179
|
-
*
|
|
6166
|
+
* - a **plugin entry** anchors to the directory holding the file that declared it --
|
|
6167
|
+
* `<root>/.codework/` for a project entry, `<home>/` for a user one, the named directory for
|
|
6168
|
+
* `--user-config-dir`. The rule is about the file, not the project.
|
|
6169
|
+
* - a **CLI argument** anchors to the process's startup directory, because that is where the
|
|
6170
|
+
* person typed it.
|
|
6180
6171
|
*/
|
|
6181
|
-
const
|
|
6182
|
-
const
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
};
|
|
6194
|
-
/** Source metadata exists for diagnostics; a silent ID replacement is where it earns that. */
|
|
6195
|
-
const note = (plugin, source) => plugins.has(plugin.id) ? Effect.logDebug(`plugin ${plugin.id} redefined by ${source}; the earlier definition is discarded`) : Effect.void;
|
|
6196
|
-
for (const builtin of options.builtins) remember(yield* validate(builtin, {
|
|
6197
|
-
index: -1,
|
|
6198
|
-
reference: builtin.id
|
|
6199
|
-
}, true), []);
|
|
6200
|
-
for (const [index, reference] of references.entries()) {
|
|
6201
|
-
if (isPatch(reference)) continue;
|
|
6202
|
-
const declared = typeof reference === "string" ? reference : Predicate.hasProperty(reference, "id") ? reference.id : void 0;
|
|
6203
|
-
const named = typeof declared === "string" ? declared : `<plugin object #${index}>`;
|
|
6204
|
-
const origin = {
|
|
6205
|
-
index,
|
|
6206
|
-
reference: named,
|
|
6207
|
-
file: options.declared?.get(named)
|
|
6172
|
+
const parse$2 = (reference, from) => Effect.gen(function* () {
|
|
6173
|
+
const fail = (reason, message) => new SourceError({
|
|
6174
|
+
reason,
|
|
6175
|
+
reference,
|
|
6176
|
+
message
|
|
6177
|
+
});
|
|
6178
|
+
rooted(from, "the directory a plugin reference anchors to");
|
|
6179
|
+
if (reference.startsWith("file:")) {
|
|
6180
|
+
if (!reference.startsWith("file:///")) return yield* fail("plugin-unsupported-source", `not an absolute file URL: ${reference}`);
|
|
6181
|
+
return {
|
|
6182
|
+
kind: "local",
|
|
6183
|
+
path: fileURLToPath(reference)
|
|
6208
6184
|
};
|
|
6209
|
-
if (typeof reference !== "string") {
|
|
6210
|
-
if (typeof declared === "string" && plugins.get(declared) === reference) continue;
|
|
6211
|
-
const plugin = yield* validate(reference, origin);
|
|
6212
|
-
yield* note(plugin, "a supplied object");
|
|
6213
|
-
remember(plugin, []);
|
|
6214
|
-
continue;
|
|
6215
|
-
}
|
|
6216
|
-
const target = yield* parse$2(reference, options.hostDir);
|
|
6217
|
-
const key = target.kind === "local" ? target.path : target.spec;
|
|
6218
|
-
const already = seen.get(key);
|
|
6219
|
-
if (already !== void 0) {
|
|
6220
|
-
aliases.set(reference, already);
|
|
6221
|
-
continue;
|
|
6222
|
-
}
|
|
6223
|
-
const definition = yield* load$3(target, origin, options);
|
|
6224
|
-
yield* note(definition.plugin, definition.version === void 0 ? key : `${key}@${definition.version}`);
|
|
6225
|
-
const names = target.kind === "local" ? [target.path, ...definition.name === void 0 ? [] : [definition.name]] : [canonical$1(target), target.spec];
|
|
6226
|
-
remember(definition.plugin, [reference, ...names], {
|
|
6227
|
-
...definition.version === void 0 ? {} : { version: definition.version },
|
|
6228
|
-
origin: {
|
|
6229
|
-
reference,
|
|
6230
|
-
...definition.generation === void 0 ? {} : { generation: definition.generation },
|
|
6231
|
-
...origin.file === void 0 ? {} : { file: origin.file }
|
|
6232
|
-
}
|
|
6233
|
-
});
|
|
6234
|
-
seen.set(key, definition.plugin.id);
|
|
6235
6185
|
}
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
origins
|
|
6186
|
+
const expanded = expandTilde(reference, hostPath);
|
|
6187
|
+
if (isPath(expanded)) return {
|
|
6188
|
+
kind: "local",
|
|
6189
|
+
path: hostPath.resolve(from, expanded)
|
|
6241
6190
|
};
|
|
6191
|
+
const parsed = yield* Effect.try({
|
|
6192
|
+
try: () => npa(reference),
|
|
6193
|
+
catch: () => fail("plugin-unsupported-source", `cannot parse plugin source: ${reference}`)
|
|
6194
|
+
});
|
|
6195
|
+
if (parsed.type === "git") {
|
|
6196
|
+
const { gitSubdir } = parsed;
|
|
6197
|
+
return {
|
|
6198
|
+
kind: "git",
|
|
6199
|
+
slug: slug(reference),
|
|
6200
|
+
spec: reference,
|
|
6201
|
+
...parsed.gitCommittish == null ? {} : { committish: parsed.gitCommittish },
|
|
6202
|
+
...gitSubdir == null ? {} : { subdir: gitSubdir },
|
|
6203
|
+
mutable: !isCommit(parsed.gitCommittish)
|
|
6204
|
+
};
|
|
6205
|
+
}
|
|
6206
|
+
if (parsed.name !== null && [
|
|
6207
|
+
"version",
|
|
6208
|
+
"range",
|
|
6209
|
+
"tag"
|
|
6210
|
+
].includes(parsed.type)) {
|
|
6211
|
+
const omitted = parsed.raw === parsed.name || parsed.raw === `${parsed.name}@`;
|
|
6212
|
+
return {
|
|
6213
|
+
kind: "registry",
|
|
6214
|
+
name: parsed.name,
|
|
6215
|
+
spec: `${parsed.name}@${omitted ? "latest" : parsed.rawSpec}`,
|
|
6216
|
+
mutable: parsed.type !== "version"
|
|
6217
|
+
};
|
|
6218
|
+
}
|
|
6219
|
+
return yield* fail("plugin-unsupported-source", `unsupported plugin source: ${reference}`);
|
|
6242
6220
|
});
|
|
6243
6221
|
/**
|
|
6244
|
-
*
|
|
6245
|
-
*
|
|
6246
|
-
* An array entry is a **module** — a definition object, a path, a `file:` URL, or a package
|
|
6247
|
-
* spec. It takes the position it is written at, so naming a module again moves it.
|
|
6248
|
-
*
|
|
6249
|
-
* An object entry is **configuration** for a plugin an earlier entry (or the built-in list)
|
|
6250
|
-
* already selected, addressed by its ID (`plugin`) or the module string (`package`) that selected
|
|
6251
|
-
* it. It never loads, installs or reorders anything, and a name matching nothing in the selection
|
|
6252
|
-
* is ignored rather than failing the boot — a typo costs a debug line, never a fetch.
|
|
6222
|
+
* What two references must share to be the same plugin, without installing either.
|
|
6253
6223
|
*
|
|
6254
|
-
*
|
|
6255
|
-
*
|
|
6224
|
+
* A local path is the file it resolves to; a package is its name, so `@acme/x@1` and `@acme/x@2`
|
|
6225
|
+
* are one package at two versions. Pure -- nothing is installed, imported or read.
|
|
6256
6226
|
*/
|
|
6257
|
-
const
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
index,
|
|
6263
|
-
options: operations.get(id)?.options ?? {}
|
|
6264
|
-
});
|
|
6265
|
-
for (const [index, reference] of references.entries()) {
|
|
6266
|
-
if (isPatch(reference)) {
|
|
6267
|
-
const named = reference.plugin ?? reference.package;
|
|
6268
|
-
const origin = {
|
|
6269
|
-
index,
|
|
6270
|
-
reference: typeof named === "string" ? named : `<plugin entry #${index}>`
|
|
6271
|
-
};
|
|
6272
|
-
const invalid = reference.plugin !== void 0 && reference.package !== void 0 ? "names both `plugin` and `package`" : typeof named !== "string" || named.length === 0 ? "names neither `plugin` nor `package`" : reference.enabled !== void 0 && typeof reference.enabled !== "boolean" ? "`enabled` is not a boolean" : reference.options !== void 0 && !isRecord$1(reference.options) ? "`options` is not a plain object" : void 0;
|
|
6273
|
-
if (invalid !== void 0) return yield* definitionFailure(origin, /* @__PURE__ */ new Error(`plugin entry ${invalid}`));
|
|
6274
|
-
const id = reference.plugin === void 0 ? pool.aliases.get(reference.package) : reference.plugin;
|
|
6275
|
-
const operation = id === void 0 ? void 0 : operations.get(id);
|
|
6276
|
-
if (id === void 0 || operation === void 0) {
|
|
6277
|
-
yield* Effect.logDebug(`plugin configuration ignored: nothing selected under ${named}`);
|
|
6278
|
-
continue;
|
|
6279
|
-
}
|
|
6280
|
-
operations.set(id, {
|
|
6281
|
-
enabled: reference.enabled ?? operation.enabled,
|
|
6282
|
-
index: operation.index,
|
|
6283
|
-
options: reference.options ?? operation.options
|
|
6284
|
-
});
|
|
6285
|
-
continue;
|
|
6286
|
-
}
|
|
6287
|
-
if (typeof reference !== "string") {
|
|
6288
|
-
const declared = Predicate.hasProperty(reference, "id") ? reference.id : void 0;
|
|
6289
|
-
if (typeof declared === "string" && pool.plugins.has(declared)) choose(declared, index);
|
|
6290
|
-
continue;
|
|
6291
|
-
}
|
|
6292
|
-
const id = pool.aliases.get(reference);
|
|
6293
|
-
if (id === void 0) missing.push(reference);
|
|
6294
|
-
else choose(id, index);
|
|
6295
|
-
}
|
|
6296
|
-
const selection = [];
|
|
6297
|
-
for (const [id, operation] of operations) {
|
|
6298
|
-
if (!operation.enabled) continue;
|
|
6299
|
-
const plugin = pool.plugins.get(id);
|
|
6300
|
-
if (plugin !== void 0) selection.push({
|
|
6301
|
-
plugin,
|
|
6302
|
-
options: Object.freeze(operation.options),
|
|
6303
|
-
index: operation.index
|
|
6304
|
-
});
|
|
6227
|
+
const canonical$1 = (target) => {
|
|
6228
|
+
switch (target.kind) {
|
|
6229
|
+
case "local": return target.path;
|
|
6230
|
+
case "registry": return target.name;
|
|
6231
|
+
case "git": return target.spec;
|
|
6305
6232
|
}
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6233
|
+
};
|
|
6234
|
+
//#endregion
|
|
6235
|
+
//#region src/plugin/index.store.ts
|
|
6236
|
+
const Record_ = Schema.Struct({
|
|
6237
|
+
spec: Schema.String,
|
|
6238
|
+
source: Schema.Literals(["registry", "git"]),
|
|
6239
|
+
name: Schema.String,
|
|
6240
|
+
slug: Schema.String,
|
|
6241
|
+
mutable: Schema.Boolean,
|
|
6242
|
+
generation: Schema.Finite,
|
|
6243
|
+
version: Schema.optional(Schema.String),
|
|
6244
|
+
revision: Schema.optional(Schema.String),
|
|
6245
|
+
entrypoint: Schema.String,
|
|
6246
|
+
installedAt: Schema.Finite,
|
|
6247
|
+
checkedAt: Schema.optional(Schema.Finite),
|
|
6248
|
+
outdated: Schema.optional(Schema.Boolean),
|
|
6249
|
+
/** The remote revision observed at `checkedAt`; distinct from the installed `revision`. */
|
|
6250
|
+
available: Schema.optional(Schema.String)
|
|
6251
|
+
});
|
|
6252
|
+
const File = Schema.Struct({
|
|
6253
|
+
version: Schema.Finite,
|
|
6254
|
+
entries: Schema.Record(Schema.String, Record_)
|
|
6314
6255
|
});
|
|
6256
|
+
const VERSION = 1;
|
|
6257
|
+
const NAME = "index.json";
|
|
6315
6258
|
/**
|
|
6316
|
-
*
|
|
6259
|
+
* Read the whole file, or start empty.
|
|
6317
6260
|
*
|
|
6318
|
-
*
|
|
6319
|
-
*
|
|
6320
|
-
|
|
6261
|
+
* An unreadable or unparseable index is not an error: it is a cold cache. Failing here would make
|
|
6262
|
+
* a corrupt accelerator break installs that do not need it.
|
|
6263
|
+
*/
|
|
6264
|
+
const read = Effect.fn("PluginIndex.read")(function* (root) {
|
|
6265
|
+
const parsed = yield* fileSystem.readFileString(hostPath.join(root, NAME)).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(File))), Effect.option);
|
|
6266
|
+
if (Option.isNone(parsed) || parsed.value.version !== VERSION) return /* @__PURE__ */ new Map();
|
|
6267
|
+
return new Map(Object.entries(parsed.value.entries));
|
|
6268
|
+
});
|
|
6269
|
+
/** Temp file plus rename, so a reader never sees a half-written index. */
|
|
6270
|
+
const write = Effect.fn("PluginIndex.write")(function* (root, entries) {
|
|
6271
|
+
const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(File))({
|
|
6272
|
+
version: VERSION,
|
|
6273
|
+
entries: Object.fromEntries(entries)
|
|
6274
|
+
});
|
|
6275
|
+
yield* fileSystem.makeDirectory(root, { recursive: true });
|
|
6276
|
+
const temporary = yield* fileSystem.makeTempFile({
|
|
6277
|
+
directory: root,
|
|
6278
|
+
prefix: `.${NAME}.`,
|
|
6279
|
+
suffix: ".tmp"
|
|
6280
|
+
});
|
|
6281
|
+
yield* fileSystem.writeFileString(temporary, encoded).pipe(Effect.andThen(fileSystem.rename(temporary, hostPath.join(root, NAME))), Effect.ensuring(fileSystem.remove(temporary, { force: true }).pipe(Effect.ignore)));
|
|
6282
|
+
});
|
|
6283
|
+
/**
|
|
6284
|
+
* Record one entry, keeping whatever else the file holds.
|
|
6321
6285
|
*
|
|
6322
|
-
*
|
|
6323
|
-
*
|
|
6286
|
+
* Ignores its own failures. A store that cannot write its accelerator still works; refusing the
|
|
6287
|
+
* install would be the accelerator deciding whether the install counted.
|
|
6288
|
+
*/
|
|
6289
|
+
const put = Effect.fn("PluginIndex.put")(function* (root, digest, record) {
|
|
6290
|
+
const entries = yield* read(root);
|
|
6291
|
+
entries.set(digest, record);
|
|
6292
|
+
yield* write(root, entries).pipe(Effect.ignore);
|
|
6293
|
+
});
|
|
6294
|
+
/**
|
|
6295
|
+
* Merge fields into one record, if it is there.
|
|
6324
6296
|
*
|
|
6325
|
-
*
|
|
6326
|
-
*
|
|
6327
|
-
*
|
|
6297
|
+
* Unlocked, like every write here but the one that publishes a generation. Two processes checking
|
|
6298
|
+
* at once both read the whole file and both rewrite it, so one update is lost and a record added
|
|
6299
|
+
* in between can vanish -- and neither matters, because a lost `checkedAt` costs one network call
|
|
6300
|
+
* and a vanished record is a miss a scan repairs.
|
|
6301
|
+
*/
|
|
6302
|
+
const patch = Effect.fn("PluginIndex.patch")(function* (root, digest, fields) {
|
|
6303
|
+
const entries = yield* read(root);
|
|
6304
|
+
const existing = entries.get(digest);
|
|
6305
|
+
if (existing === void 0) return;
|
|
6306
|
+
entries.set(digest, {
|
|
6307
|
+
...existing,
|
|
6308
|
+
...fields
|
|
6309
|
+
});
|
|
6310
|
+
yield* write(root, entries).pipe(Effect.ignore);
|
|
6311
|
+
});
|
|
6312
|
+
/** Forget one entry, for a store entry that has been removed. */
|
|
6313
|
+
const drop = Effect.fn("PluginIndex.drop")(function* (root, digest) {
|
|
6314
|
+
const entries = yield* read(root);
|
|
6315
|
+
if (!entries.delete(digest)) return;
|
|
6316
|
+
yield* write(root, entries).pipe(Effect.ignore);
|
|
6317
|
+
});
|
|
6318
|
+
//#endregion
|
|
6319
|
+
//#region src/plugin/lock.ts
|
|
6320
|
+
/** How long to wait for another holder before giving up. */
|
|
6321
|
+
const TIMEOUT = Duration.minutes(2);
|
|
6322
|
+
/** The holder refreshes the lock's mtime this often; one not refreshed for the timeout is stale. */
|
|
6323
|
+
const HEARTBEAT = Duration.seconds(15);
|
|
6324
|
+
const abandoned = (directory) => Effect.gen(function* () {
|
|
6325
|
+
const info = yield* fileSystem.stat(directory);
|
|
6326
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
6327
|
+
return Option.exists(info.mtime, (mtime) => now - mtime.getTime() > Duration.toMillis(TIMEOUT));
|
|
6328
|
+
}).pipe(Effect.orElseSucceed(() => false));
|
|
6329
|
+
const heartbeat = (directory) => Effect.clockWith((clock) => clock.currentTimeMillis).pipe(Effect.flatMap((now) => fileSystem.utimes(directory, now, now)), Effect.ignore, Effect.repeat(Schedule.spaced(HEARTBEAT)));
|
|
6330
|
+
/**
|
|
6331
|
+
* Claims `directory` by creating it, polling while another process holds it, and releasing it when
|
|
6332
|
+
* the surrounding scope closes.
|
|
6328
6333
|
*
|
|
6329
|
-
*
|
|
6330
|
-
*
|
|
6334
|
+
* One finalizer is registered for the whole wait rather than one per 50ms poll, and a single
|
|
6335
|
+
* attempt is `uninterruptible` so `mkdir` and recording ownership cannot be split -- an interrupt
|
|
6336
|
+
* between them would leave behind a lock the finalizer does not know to remove.
|
|
6331
6337
|
*
|
|
6332
|
-
*
|
|
6333
|
-
* already paying for the I/O that would tell us, and discarding the answer. Three bounds keep it
|
|
6334
|
-
* safe: the store only and never the network, so an exchange cannot block on a registry;
|
|
6335
|
-
* resolve-only, so it cannot install; and once, at the top of a snapshot, so an exchange already
|
|
6336
|
-
* running is unaffected.
|
|
6338
|
+
* `onTimeout` names the failure, because what a caller is waiting for is the caller's to describe.
|
|
6337
6339
|
*/
|
|
6338
|
-
const
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
if (!moved) return Option.none();
|
|
6356
|
-
const retained = [];
|
|
6357
|
-
for (const origin of pool.origins.values()) {
|
|
6358
|
-
if (references.includes(origin.reference) || Option.isSome(yield* filed(origin.reference, anchor$1(origin.file, options.hostDir)))) {
|
|
6359
|
-
retained.push(origin);
|
|
6360
|
-
continue;
|
|
6361
|
-
}
|
|
6362
|
-
yield* Effect.logWarning(`plugin ${origin.reference} no longer resolves; it is dropped from the loaded set`);
|
|
6363
|
-
}
|
|
6364
|
-
const accumulated = [...Array.from(retained, (origin) => origin.reference), ...references];
|
|
6365
|
-
const known = /* @__PURE__ */ new Map();
|
|
6366
|
-
for (const origin of retained) if (origin.file !== void 0) known.set(origin.reference, origin.file);
|
|
6367
|
-
for (const [reference, file] of options.declared ?? /* @__PURE__ */ new Map()) known.set(reference, file);
|
|
6368
|
-
return Option.some(yield* load$2(accumulated, {
|
|
6369
|
-
...options,
|
|
6370
|
-
declared: known
|
|
6371
|
-
}));
|
|
6372
|
-
});
|
|
6373
|
-
Effect.fn("PluginCatalog.prepare")(function* (references, options) {
|
|
6374
|
-
const pool = yield* load$2(references, options);
|
|
6375
|
-
return (yield* select(references, pool)).selection;
|
|
6340
|
+
const lock = Effect.fn("PluginLock.acquire")(function* (directory, onTimeout) {
|
|
6341
|
+
const held = yield* Ref.make(false);
|
|
6342
|
+
yield* Effect.acquireRelease(Effect.void, () => Ref.get(held).pipe(Effect.flatMap((owned) => owned ? fileSystem.remove(directory, {
|
|
6343
|
+
recursive: true,
|
|
6344
|
+
force: true
|
|
6345
|
+
}) : Effect.void), Effect.orDie));
|
|
6346
|
+
if (!(yield* fileSystem.makeDirectory(directory).pipe(Effect.andThen(Ref.set(held, true)), Effect.as(true), Effect.catchIf((error) => error.reason._tag === "AlreadyExists", () => abandoned(directory).pipe(Effect.flatMap((stale) => stale ? fileSystem.remove(directory, {
|
|
6347
|
+
recursive: true,
|
|
6348
|
+
force: true
|
|
6349
|
+
}).pipe(Effect.as(false)) : Effect.succeed(false)))), Effect.uninterruptible).pipe(Effect.repeat({
|
|
6350
|
+
schedule: Schedule.spaced("50 millis"),
|
|
6351
|
+
until: (owned) => owned
|
|
6352
|
+
}), Effect.timeoutOrElse({
|
|
6353
|
+
duration: TIMEOUT,
|
|
6354
|
+
orElse: () => Effect.succeed(false)
|
|
6355
|
+
})))) return yield* Effect.fail(onTimeout());
|
|
6356
|
+
yield* Effect.forkScoped(heartbeat(directory));
|
|
6376
6357
|
});
|
|
6377
6358
|
//#endregion
|
|
6378
|
-
//#region src/plugin/
|
|
6379
|
-
/**
|
|
6380
|
-
const
|
|
6359
|
+
//#region src/plugin/store.ts
|
|
6360
|
+
/** Facts about the *package*, and nothing about the plugin inside it. */
|
|
6361
|
+
const Marker = Schema.Struct({
|
|
6362
|
+
spec: Schema.String,
|
|
6363
|
+
name: Schema.String,
|
|
6364
|
+
version: Schema.optional(Schema.String),
|
|
6365
|
+
revision: Schema.optional(Schema.String),
|
|
6366
|
+
/** Relative to the generation directory, so the marker survives the publishing rename. */
|
|
6367
|
+
entrypoint: Schema.String,
|
|
6368
|
+
createdAt: Schema.Finite
|
|
6369
|
+
});
|
|
6370
|
+
const MARKER = ".complete.json";
|
|
6371
|
+
const STAGING = ".staging-";
|
|
6372
|
+
const root = (cache) => hostPath.join(cache, "plugins", "v2");
|
|
6381
6373
|
/**
|
|
6382
|
-
*
|
|
6374
|
+
* A readable label, never an identity: two specs that slug alike share a parent directory and
|
|
6375
|
+
* nothing more. Worth having because a greppable path is worth real money when debugging, and a
|
|
6376
|
+
* bare hash throws away every clue.
|
|
6377
|
+
*/
|
|
6378
|
+
const slugOf = (target) => target.kind === "git" ? target.slug : canonical$1(target).replace(/^@/, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "package";
|
|
6379
|
+
/**
|
|
6380
|
+
* The **full** SHA-256 of the source identity: canonical spec plus effective registry for a
|
|
6381
|
+
* registry package, and the canonical spec alone for git.
|
|
6383
6382
|
*
|
|
6384
|
-
*
|
|
6385
|
-
*
|
|
6383
|
+
* Not truncated. The fast path proves a hit by checking that a marker *exists* rather than by
|
|
6384
|
+
* decoding it, and the index is keyed by digest alone, so a truncated digest would let a collision
|
|
6385
|
+
* return the wrong package silently.
|
|
6386
6386
|
*/
|
|
6387
|
-
const
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6387
|
+
const digest = Effect.fn("PluginStore.digest")(function* (target, cache, from) {
|
|
6388
|
+
const source = target.kind === "registry" ? `${yield* registry(target, cache, from)}\0${target.spec}` : target.spec;
|
|
6389
|
+
return Encoding.encodeHex(yield* crypto.digest("SHA-256", new TextEncoder().encode(source)));
|
|
6390
|
+
});
|
|
6391
|
+
const entryDir = Effect.fn("PluginStore.entryDir")(function* (target, cache, from) {
|
|
6392
|
+
const key = yield* digest(target, cache, from);
|
|
6393
|
+
return {
|
|
6394
|
+
key,
|
|
6395
|
+
directory: hostPath.join(root(cache), slugOf(target), key)
|
|
6396
|
+
};
|
|
6397
|
+
});
|
|
6398
|
+
const failure$1 = (target, reason, message, cause) => new StoreError({
|
|
6399
|
+
reason,
|
|
6400
|
+
reference: target.spec,
|
|
6401
|
+
message,
|
|
6402
|
+
...cause === void 0 ? {} : { cause }
|
|
6403
|
+
});
|
|
6404
|
+
/** Generation directories, oldest first. A name that is not a number is not a generation. */
|
|
6405
|
+
const generations = Effect.fn("PluginStore.generations")(function* (directory) {
|
|
6406
|
+
return (yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []))).filter((name) => /^\d+$/.test(name)).map((name) => Number(name)).sort((left, right) => left - right);
|
|
6407
|
+
});
|
|
6408
|
+
const readMarker = (directory) => fileSystem.readFileString(hostPath.join(directory, MARKER)).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Marker))), Effect.option);
|
|
6409
|
+
const entryOf = (marker, input) => ({
|
|
6410
|
+
...marker,
|
|
6411
|
+
digest: input.digest,
|
|
6412
|
+
generation: input.generation,
|
|
6413
|
+
directory: input.directory,
|
|
6414
|
+
url: url(hostPath.resolve(input.directory, marker.entrypoint))
|
|
6415
|
+
});
|
|
6416
|
+
/** The newest generation that carries a marker. Unmarked ones are invisible: they never happened. */
|
|
6417
|
+
const active = Effect.fn("PluginStore.active")(function* (directory, key) {
|
|
6418
|
+
for (const generation of (yield* generations(directory)).reverse()) {
|
|
6419
|
+
const where = hostPath.join(directory, String(generation));
|
|
6420
|
+
const marker = yield* readMarker(where);
|
|
6421
|
+
if (Option.isSome(marker)) return entryOf(marker.value, {
|
|
6422
|
+
digest: key,
|
|
6423
|
+
generation,
|
|
6424
|
+
directory: where
|
|
6425
|
+
});
|
|
6426
|
+
}
|
|
6427
|
+
});
|
|
6428
|
+
/** What the index files about an entry, all of it derivable from the spec and the marker. */
|
|
6429
|
+
const record = (target, entry, installedAt) => ({
|
|
6430
|
+
spec: target.spec,
|
|
6431
|
+
source: target.kind,
|
|
6432
|
+
name: entry.name,
|
|
6433
|
+
slug: slugOf(target),
|
|
6434
|
+
mutable: target.mutable,
|
|
6435
|
+
generation: entry.generation,
|
|
6436
|
+
...entry.version === void 0 ? {} : { version: entry.version },
|
|
6437
|
+
...entry.revision === void 0 ? {} : { revision: entry.revision },
|
|
6438
|
+
entrypoint: entry.entrypoint,
|
|
6439
|
+
installedAt
|
|
6440
|
+
});
|
|
6394
6441
|
/**
|
|
6395
|
-
*
|
|
6396
|
-
*
|
|
6442
|
+
* Disk-only lookup. No network, no install -- this is what boot uses, and the reason booting
|
|
6443
|
+
* offline works.
|
|
6397
6444
|
*
|
|
6398
|
-
*
|
|
6399
|
-
*
|
|
6445
|
+
* The index is consulted first and is only ever a hint: a hit is proved by the marker still
|
|
6446
|
+
* existing, and anything else falls through to a scan that repairs the index on the way past.
|
|
6400
6447
|
*/
|
|
6401
|
-
const
|
|
6402
|
-
const
|
|
6403
|
-
const
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6448
|
+
const resolve$2 = Effect.fn("PluginStore.resolve")(function* (target, cache, from) {
|
|
6449
|
+
const { key, directory } = yield* entryDir(target, cache, from);
|
|
6450
|
+
const base = root(cache);
|
|
6451
|
+
const hinted = (yield* read(base)).get(key);
|
|
6452
|
+
if (hinted !== void 0) {
|
|
6453
|
+
const where = hostPath.join(directory, String(hinted.generation));
|
|
6454
|
+
const marker = yield* readMarker(where);
|
|
6455
|
+
if (Option.isSome(marker)) return entryOf(marker.value, {
|
|
6456
|
+
digest: key,
|
|
6457
|
+
generation: hinted.generation,
|
|
6458
|
+
directory: where
|
|
6459
|
+
});
|
|
6409
6460
|
}
|
|
6410
|
-
|
|
6411
|
-
|
|
6461
|
+
const scanned = yield* active(directory, key);
|
|
6462
|
+
if (scanned === void 0) return void 0;
|
|
6463
|
+
yield* put(base, key, record(target, scanned, scanned.createdAt));
|
|
6464
|
+
return scanned;
|
|
6465
|
+
}, Effect.mapError((cause) => Schema.is(StoreError)(cause) || Schema.is(InstallError)(cause) ? cause : new StoreError({
|
|
6466
|
+
reason: "plugin-marker-invalid",
|
|
6467
|
+
reference: "",
|
|
6468
|
+
message: "cannot read the plugin store",
|
|
6469
|
+
cause
|
|
6470
|
+
})));
|
|
6412
6471
|
/**
|
|
6413
|
-
*
|
|
6472
|
+
* `resolve`, for a caller that needs the entry rather than an answer about it.
|
|
6414
6473
|
*
|
|
6415
|
-
* `
|
|
6416
|
-
*
|
|
6417
|
-
|
|
6418
|
-
const toolIndex = (tools) => {
|
|
6419
|
-
const listed = tools.filter((tool) => tool.promptSnippet !== void 0 && tool.promptSnippet.length > 0);
|
|
6420
|
-
if (listed.length === 0) return "(none)";
|
|
6421
|
-
return listed.map((tool) => `- ${tool.name}: ${tool.promptSnippet}`).join("\n");
|
|
6422
|
-
};
|
|
6423
|
-
const assemble = (input) => {
|
|
6424
|
-
const sections = [input.promptCustom ?? foundation];
|
|
6425
|
-
sections.push(`Available tools:\n${toolIndex(input.tools)}`);
|
|
6426
|
-
const lines = guidelines(input.tools);
|
|
6427
|
-
if (lines.length > 0) sections.push(`Guidelines:\n${lines.map((line) => `- ${line}`).join("\n")}`);
|
|
6428
|
-
const append = input.promptSystemAppend?.trim();
|
|
6429
|
-
if (append !== void 0 && append.length > 0) sections.push(append);
|
|
6430
|
-
sections.push(`Current working directory: ${input.directory}`);
|
|
6431
|
-
return sections.join("\n\n");
|
|
6432
|
-
};
|
|
6433
|
-
/**
|
|
6434
|
-
* A caller slot, awaited lazily. A throw or rejection fails the snapshot, which the
|
|
6435
|
-
* host attributes to this plugin's id.
|
|
6474
|
+
* `plugin remove` uses this as its installer: it has to identify what an entry names without
|
|
6475
|
+
* fetching anything, and a spec with nothing filed is a real answer there -- the command reports
|
|
6476
|
+
* it and moves on.
|
|
6436
6477
|
*/
|
|
6437
|
-
const
|
|
6438
|
-
const
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
setup: Effect.fn("DefaultPromptPlugin.setup")(function* (ctx) {
|
|
6442
|
-
const custom = yield* slot(ctx, ctx.config.promptCustom);
|
|
6443
|
-
const append = yield* slot(ctx, ctx.config.promptSystemAppend);
|
|
6444
|
-
ctx.plugin.prompt.set(assemble({
|
|
6445
|
-
tools: ctx.plugin.tools.list(),
|
|
6446
|
-
directory: ctx.location.directory,
|
|
6447
|
-
...custom === void 0 ? {} : { promptCustom: custom },
|
|
6448
|
-
...append === void 0 ? {} : { promptSystemAppend: append }
|
|
6449
|
-
}));
|
|
6450
|
-
})
|
|
6478
|
+
const required = Effect.fn("PluginStore.required")(function* (target, cache, from) {
|
|
6479
|
+
const found = yield* resolve$2(target, cache, from);
|
|
6480
|
+
if (found === void 0) return yield* failure$1(target, "plugin-not-installed", `${target.spec} is not installed`);
|
|
6481
|
+
return found;
|
|
6451
6482
|
});
|
|
6452
|
-
//#endregion
|
|
6453
|
-
//#region src/tool/truncate.ts
|
|
6454
6483
|
/**
|
|
6455
|
-
*
|
|
6456
|
-
*
|
|
6457
|
-
* Truncation is based on two independent limits — whichever is hit first
|
|
6458
|
-
* wins: a line limit and a byte limit. Lines are never split (except the bash
|
|
6459
|
-
* tail edge case where a single line exceeds the byte limit).
|
|
6484
|
+
* Install if absent; hand back what is filed if present.
|
|
6460
6485
|
*
|
|
6461
|
-
*
|
|
6462
|
-
*
|
|
6463
|
-
*/
|
|
6464
|
-
const DEFAULT_MAX_LINES = 2e3;
|
|
6465
|
-
const DEFAULT_MAX_BYTES = 51200;
|
|
6466
|
-
function splitLinesForCounting(content) {
|
|
6467
|
-
if (content.length === 0) return [];
|
|
6468
|
-
const lines = content.split("\n");
|
|
6469
|
-
if (content.endsWith("\n")) lines.pop();
|
|
6470
|
-
return lines;
|
|
6471
|
-
}
|
|
6472
|
-
/** Format bytes as a human-readable size. */
|
|
6473
|
-
function formatSize(bytes) {
|
|
6474
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
6475
|
-
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
6476
|
-
return `${(bytes / 1048576).toFixed(1)}MB`;
|
|
6477
|
-
}
|
|
6478
|
-
/**
|
|
6479
|
-
* Truncate content from the tail (keep the last N lines/bytes). Suitable for
|
|
6480
|
-
* bash output where the end matters (errors, final results). May return a
|
|
6481
|
-
* partial first line if the last line of the original content exceeds the byte
|
|
6482
|
-
* limit.
|
|
6486
|
+
* `refresh` forces past both the fast path and the double-check, which is what `update` needs and
|
|
6487
|
+
* nothing else should use.
|
|
6483
6488
|
*/
|
|
6484
|
-
function
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
if (totalLines <= maxLines && totalBytes <= maxBytes) return {
|
|
6491
|
-
content,
|
|
6492
|
-
truncated: false,
|
|
6493
|
-
truncatedBy: null,
|
|
6494
|
-
totalLines,
|
|
6495
|
-
totalBytes,
|
|
6496
|
-
outputLines: totalLines,
|
|
6497
|
-
outputBytes: totalBytes,
|
|
6498
|
-
lastLinePartial: false,
|
|
6499
|
-
firstLineExceedsLimit: false,
|
|
6500
|
-
maxLines,
|
|
6501
|
-
maxBytes
|
|
6502
|
-
};
|
|
6503
|
-
const outputLinesArr = [];
|
|
6504
|
-
let outputBytesCount = 0;
|
|
6505
|
-
let truncatedBy = "lines";
|
|
6506
|
-
let lastLinePartial = false;
|
|
6507
|
-
for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
|
|
6508
|
-
const line = lines[i] ?? "";
|
|
6509
|
-
const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0);
|
|
6510
|
-
if (outputBytesCount + lineBytes > maxBytes) {
|
|
6511
|
-
truncatedBy = "bytes";
|
|
6512
|
-
if (outputLinesArr.length === 0) {
|
|
6513
|
-
const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
|
|
6514
|
-
outputLinesArr.unshift(truncatedLine);
|
|
6515
|
-
outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
|
|
6516
|
-
lastLinePartial = true;
|
|
6517
|
-
}
|
|
6518
|
-
break;
|
|
6489
|
+
const add = Effect.fn("PluginStore.add")(function* (target, cache, options) {
|
|
6490
|
+
return yield* Effect.gen(function* () {
|
|
6491
|
+
const { key, directory } = yield* entryDir(target, cache, options.from);
|
|
6492
|
+
if (options.refresh !== true) {
|
|
6493
|
+
const found = yield* resolve$2(target, cache, options.from);
|
|
6494
|
+
if (found !== void 0) return { entry: found };
|
|
6519
6495
|
}
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
}
|
|
6540
|
-
/**
|
|
6541
|
-
* Truncate a string to fit within a byte limit (from the end). Handles
|
|
6542
|
-
* multi-byte UTF-8 characters correctly (never splits a codepoint).
|
|
6543
|
-
*/
|
|
6544
|
-
function truncateStringToBytesFromEnd(str, maxBytes) {
|
|
6545
|
-
const buf = Buffer.from(str, "utf-8");
|
|
6546
|
-
if (buf.length <= maxBytes) return str;
|
|
6547
|
-
let start = buf.length - maxBytes;
|
|
6548
|
-
while (start < buf.length && (buf[start] & 192) === 128) start++;
|
|
6549
|
-
return buf.subarray(start).toString("utf-8");
|
|
6550
|
-
}
|
|
6551
|
-
//#endregion
|
|
6552
|
-
//#region src/tool/accumulator.ts
|
|
6553
|
-
function defaultTempFilePath(prefix, suffix) {
|
|
6554
|
-
return posix.join(tmpdir(), `${prefix}-${suffix}.log`);
|
|
6555
|
-
}
|
|
6556
|
-
function byteLength(text) {
|
|
6557
|
-
return Buffer.byteLength(text, "utf-8");
|
|
6558
|
-
}
|
|
6559
|
-
var Accumulator = class {
|
|
6560
|
-
maxLines;
|
|
6561
|
-
maxBytes;
|
|
6562
|
-
maxRollingBytes;
|
|
6563
|
-
tempFilePrefix;
|
|
6564
|
-
decoder = new TextDecoder();
|
|
6565
|
-
rawChunks = [];
|
|
6566
|
-
tailText = "";
|
|
6567
|
-
tailBytes = 0;
|
|
6568
|
-
tailStartsAtLineBoundary = true;
|
|
6569
|
-
totalRawBytes = 0;
|
|
6570
|
-
totalDecodedBytes = 0;
|
|
6571
|
-
completedLines = 0;
|
|
6572
|
-
totalLines = 0;
|
|
6573
|
-
currentLineBytes = 0;
|
|
6574
|
-
hasOpenLine = false;
|
|
6575
|
-
finished = false;
|
|
6576
|
-
tempFilePath;
|
|
6577
|
-
tempFile;
|
|
6578
|
-
constructor(options = {}) {
|
|
6579
|
-
this.maxLines = options.maxLines ?? 2e3;
|
|
6580
|
-
this.maxBytes = options.maxBytes ?? 51200;
|
|
6581
|
-
this.maxRollingBytes = Math.max(this.maxBytes * 2, 1);
|
|
6582
|
-
this.tempFilePrefix = options.tempFilePrefix ?? "codework-output";
|
|
6583
|
-
}
|
|
6584
|
-
append(data) {
|
|
6585
|
-
return Effect.suspend(() => {
|
|
6586
|
-
if (this.finished) return Effect.die(/* @__PURE__ */ new Error("cannot append to a finished output accumulator"));
|
|
6587
|
-
this.totalRawBytes += data.length;
|
|
6588
|
-
this.appendDecodedText(this.decoder.decode(data, { stream: true }));
|
|
6589
|
-
if (this.tempFile !== void 0 || this.shouldUseTempFile()) return Effect.gen({ self: this }, function* () {
|
|
6590
|
-
yield* this.ensureTempFile();
|
|
6591
|
-
yield* this.tempFile.writeAll(data).pipe(Effect.orDie);
|
|
6592
|
-
});
|
|
6593
|
-
if (data.length > 0) this.rawChunks.push(data);
|
|
6594
|
-
return Effect.void;
|
|
6595
|
-
});
|
|
6596
|
-
}
|
|
6597
|
-
finish() {
|
|
6598
|
-
return Effect.suspend(() => {
|
|
6599
|
-
if (this.finished) return Effect.void;
|
|
6600
|
-
this.finished = true;
|
|
6601
|
-
this.appendDecodedText(this.decoder.decode());
|
|
6602
|
-
return Effect.gen({ self: this }, function* () {
|
|
6603
|
-
if (this.shouldUseTempFile()) yield* this.ensureTempFile();
|
|
6604
|
-
if (this.tempFile !== void 0) yield* this.tempFile.sync.pipe(Effect.orDie);
|
|
6605
|
-
});
|
|
6496
|
+
yield* fileSystem.makeDirectory(directory, { recursive: true });
|
|
6497
|
+
yield* lock(hostPath.join(directory, ".lock"), () => failure$1(target, "plugin-lock-timeout", `timed out waiting for another installation of ${target.spec}`));
|
|
6498
|
+
const current = yield* active(directory, key);
|
|
6499
|
+
if (current !== void 0 && options.refresh !== true) return { entry: current };
|
|
6500
|
+
const real = yield* fileSystem.realPath(directory).pipe(Effect.orElseSucceed(() => directory));
|
|
6501
|
+
const staging = yield* Effect.acquireRelease(fileSystem.makeTempDirectory({
|
|
6502
|
+
directory: real,
|
|
6503
|
+
prefix: STAGING
|
|
6504
|
+
}), (staged) => fileSystem.remove(staged, {
|
|
6505
|
+
recursive: true,
|
|
6506
|
+
force: true
|
|
6507
|
+
}).pipe(Effect.orDie));
|
|
6508
|
+
yield* fileSystem.writeFileString(hostPath.join(staging, "package.json"), "{\"private\":true,\"type\":\"module\"}");
|
|
6509
|
+
const fetched = yield* download({
|
|
6510
|
+
target,
|
|
6511
|
+
into: staging,
|
|
6512
|
+
cache,
|
|
6513
|
+
from: options.from,
|
|
6514
|
+
...options.runner === void 0 ? {} : { runner: options.runner }
|
|
6606
6515
|
});
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
const
|
|
6610
|
-
|
|
6611
|
-
|
|
6516
|
+
if (current !== void 0 && current.revision === fetched.revision) return { entry: current };
|
|
6517
|
+
const validated = yield* options.validate(fetched);
|
|
6518
|
+
const entrypoint = hostPath.relative(yield* fileSystem.realPath(staging), fetched.entrypoint);
|
|
6519
|
+
if (entrypoint.startsWith("..") || hostPath.isAbsolute(entrypoint)) return yield* new InstallError({
|
|
6520
|
+
reason: "plugin-no-entrypoint",
|
|
6521
|
+
reference: target.spec,
|
|
6522
|
+
message: `entrypoint escapes its installation: ${entrypoint}`
|
|
6612
6523
|
});
|
|
6613
|
-
const
|
|
6614
|
-
const
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
maxLines: this.maxLines,
|
|
6622
|
-
maxBytes: this.maxBytes
|
|
6524
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
6525
|
+
const marker = {
|
|
6526
|
+
spec: target.spec,
|
|
6527
|
+
name: fetched.name,
|
|
6528
|
+
...fetched.version === void 0 ? {} : { version: fetched.version },
|
|
6529
|
+
...fetched.revision === void 0 ? {} : { revision: fetched.revision },
|
|
6530
|
+
entrypoint,
|
|
6531
|
+
createdAt: now
|
|
6623
6532
|
};
|
|
6533
|
+
yield* fileSystem.writeFileString(hostPath.join(staging, MARKER), yield* Schema.encodeEffect(Schema.fromJsonString(Marker))(marker));
|
|
6534
|
+
const previous = (yield* generations(directory)).at(-1) ?? 0;
|
|
6535
|
+
const generation = Math.max(now, previous + 1);
|
|
6536
|
+
const published = hostPath.join(directory, String(generation));
|
|
6537
|
+
yield* fileSystem.rename(staging, published);
|
|
6538
|
+
const entry = entryOf(marker, {
|
|
6539
|
+
digest: key,
|
|
6540
|
+
generation,
|
|
6541
|
+
directory: published
|
|
6542
|
+
});
|
|
6543
|
+
yield* put(root(cache), key, record(target, entry, now));
|
|
6544
|
+
yield* collect$1(directory);
|
|
6624
6545
|
return {
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
...this.tempFilePath === void 0 ? {} : { fullOutputPath: this.tempFilePath }
|
|
6546
|
+
entry,
|
|
6547
|
+
validated
|
|
6628
6548
|
};
|
|
6629
|
-
}
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
this.hasOpenLine = tail.length > 0;
|
|
6655
|
-
}
|
|
6656
|
-
this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);
|
|
6657
|
-
}
|
|
6658
|
-
trimTail() {
|
|
6659
|
-
const buffer = Buffer.from(this.tailText, "utf-8");
|
|
6660
|
-
if (buffer.length <= this.maxRollingBytes) {
|
|
6661
|
-
this.tailBytes = buffer.length;
|
|
6662
|
-
return;
|
|
6549
|
+
}).pipe(Effect.scoped, Effect.mapError((cause) => Schema.is(InstallError)(cause) || Schema.is(StoreError)(cause) || Schema.is(LoadError)(cause) ? cause : failure$1(target, "plugin-marker-invalid", `cannot write the store entry for ${target.spec}`, cause)));
|
|
6550
|
+
});
|
|
6551
|
+
/**
|
|
6552
|
+
* Expire what is superseded.
|
|
6553
|
+
*
|
|
6554
|
+
* The two newest generations are kept regardless of age: the newest is live, and the one before it
|
|
6555
|
+
* may still be imported by an exchange in flight. Runs at the end of an `add` that published
|
|
6556
|
+
* something -- no timer and no sweeper, because installs are the only thing that creates garbage.
|
|
6557
|
+
*/
|
|
6558
|
+
const collect$1 = Effect.fn("PluginStore.collect")(function* (directory) {
|
|
6559
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
6560
|
+
const keep = new Set((yield* generations(directory)).slice(-2).map(String));
|
|
6561
|
+
const names = yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []));
|
|
6562
|
+
for (const name of names) {
|
|
6563
|
+
if (keep.has(name) || name === ".lock") continue;
|
|
6564
|
+
if (name.startsWith(STAGING)) {
|
|
6565
|
+
const started = yield* fileSystem.stat(hostPath.join(directory, name)).pipe(Effect.option);
|
|
6566
|
+
if ((Option.isNone(started) ? 0 : Option.match(started.value.mtime, {
|
|
6567
|
+
onNone: () => 0,
|
|
6568
|
+
onSome: (at) => now - at.getTime()
|
|
6569
|
+
})) > HOUR) yield* fileSystem.remove(hostPath.join(directory, name), {
|
|
6570
|
+
recursive: true,
|
|
6571
|
+
force: true
|
|
6572
|
+
}).pipe(Effect.ignore);
|
|
6573
|
+
continue;
|
|
6663
6574
|
}
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
}
|
|
6670
|
-
getSnapshotText() {
|
|
6671
|
-
if (this.tailStartsAtLineBoundary) return this.tailText;
|
|
6672
|
-
const firstNewline = this.tailText.indexOf("\n");
|
|
6673
|
-
return firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1);
|
|
6674
|
-
}
|
|
6675
|
-
shouldUseTempFile() {
|
|
6676
|
-
return this.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines;
|
|
6677
|
-
}
|
|
6678
|
-
ensureTempFile() {
|
|
6679
|
-
if (this.tempFile !== void 0) return Effect.void;
|
|
6680
|
-
return Effect.gen({ self: this }, function* () {
|
|
6681
|
-
const suffix = Encoding.encodeHex(yield* crypto.randomBytes(8));
|
|
6682
|
-
this.tempFilePath = defaultTempFilePath(this.tempFilePrefix, suffix);
|
|
6683
|
-
this.tempFile = yield* fileSystem.open(this.tempFilePath, { flag: "w" });
|
|
6684
|
-
for (const chunk of this.rawChunks) yield* this.tempFile.writeAll(chunk);
|
|
6685
|
-
this.rawChunks = [];
|
|
6686
|
-
}).pipe(Effect.orDie);
|
|
6575
|
+
if (!/^\d+$/.test(name)) continue;
|
|
6576
|
+
if (now - Number(name) > WEEK) yield* fileSystem.remove(hostPath.join(directory, name), {
|
|
6577
|
+
recursive: true,
|
|
6578
|
+
force: true
|
|
6579
|
+
}).pipe(Effect.ignore);
|
|
6687
6580
|
}
|
|
6688
|
-
};
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
var progress_exports = /* @__PURE__ */ __exportAll({});
|
|
6692
|
-
import * as import__codeworksh_plugin_tool_progress from "@codeworksh/plugin/tool/progress";
|
|
6693
|
-
__reExport(progress_exports, import__codeworksh_plugin_tool_progress);
|
|
6694
|
-
//#endregion
|
|
6695
|
-
//#region src/tool/shell.ts
|
|
6581
|
+
});
|
|
6582
|
+
const HOUR = 36e5;
|
|
6583
|
+
const WEEK = 168 * HOUR;
|
|
6696
6584
|
/**
|
|
6697
|
-
*
|
|
6585
|
+
* How long a network answer is reused before `check` asks again.
|
|
6698
6586
|
*
|
|
6699
|
-
*
|
|
6700
|
-
*
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6587
|
+
* Persisted in the index rather than held in memory, because the thing that benefits is a command
|
|
6588
|
+
* that exits: an in-memory staleness cache is worthless to `codework plugin check`.
|
|
6589
|
+
*/
|
|
6590
|
+
const CHECK_TTL = 36e5;
|
|
6591
|
+
/**
|
|
6592
|
+
* Is a newer revision available?
|
|
6704
6593
|
*
|
|
6705
|
-
*
|
|
6706
|
-
*
|
|
6707
|
-
*
|
|
6708
|
-
* - Remote / provider → {@link fromSandboxShell} over `EnvDaytona` / `EnvVercel`
|
|
6594
|
+
* The only network call in the store's vocabulary, and it is the caller's to make. A `probe` that
|
|
6595
|
+
* fails -- offline, registry down, a remote that refuses -- propagates rather than answering
|
|
6596
|
+
* `current`: an outage must not read as "you are up to date".
|
|
6709
6597
|
*/
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6598
|
+
const check = Effect.fn("PluginStore.check")(function* (target, cache, options) {
|
|
6599
|
+
if (!target.mutable) return { _tag: "immutable" };
|
|
6600
|
+
const entry = yield* resolve$2(target, cache, options.from);
|
|
6601
|
+
if (entry === void 0) return { _tag: "not-installed" };
|
|
6602
|
+
const key = yield* digest(target, cache, options.from).pipe(Effect.mapError((cause) => failure$1(target, "plugin-marker-invalid", `cannot address ${target.spec}`, cause)));
|
|
6603
|
+
const base = root(cache);
|
|
6604
|
+
const record = (yield* read(base)).get(key);
|
|
6605
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
6606
|
+
if (options.refresh !== true && record?.checkedAt !== void 0 && now - record.checkedAt < CHECK_TTL && (record.outdated !== true || record.available !== void 0)) return record.outdated === true ? {
|
|
6607
|
+
_tag: "outdated",
|
|
6608
|
+
available: record.available,
|
|
6609
|
+
...entry.revision === void 0 ? {} : { filed: entry.revision }
|
|
6610
|
+
} : {
|
|
6611
|
+
_tag: "current",
|
|
6612
|
+
...entry.revision === void 0 ? {} : { revision: entry.revision }
|
|
6613
|
+
};
|
|
6614
|
+
const available = yield* options.probe(target);
|
|
6615
|
+
if (available === void 0) return {
|
|
6616
|
+
_tag: "current",
|
|
6617
|
+
...entry.revision === void 0 ? {} : { revision: entry.revision }
|
|
6618
|
+
};
|
|
6619
|
+
const outdated = available !== entry.revision;
|
|
6620
|
+
yield* patch(base, key, {
|
|
6621
|
+
checkedAt: now,
|
|
6622
|
+
outdated,
|
|
6623
|
+
available
|
|
6624
|
+
});
|
|
6625
|
+
return outdated ? {
|
|
6626
|
+
_tag: "outdated",
|
|
6627
|
+
available,
|
|
6628
|
+
...entry.revision === void 0 ? {} : { filed: entry.revision }
|
|
6629
|
+
} : {
|
|
6630
|
+
_tag: "current",
|
|
6631
|
+
...entry.revision === void 0 ? {} : { revision: entry.revision }
|
|
6632
|
+
};
|
|
6633
|
+
});
|
|
6722
6634
|
/**
|
|
6723
|
-
*
|
|
6724
|
-
* (in-process) and remote providers.
|
|
6635
|
+
* Refresh a mutable target, if the network says there is anything to refresh.
|
|
6725
6636
|
*
|
|
6726
|
-
* `
|
|
6727
|
-
*
|
|
6728
|
-
* timeout is enforced here with `Effect.timeoutOption` (interruption always
|
|
6729
|
-
* returns control; the underlying command is only truly killed mid-flight on
|
|
6730
|
-
* signal-aware backends, handled in the provider layer). When the backend exposes
|
|
6731
|
-
* `stream` (e.g. Vercel `Command.logs`), it is bridged to {@link IToolShell.stream}
|
|
6732
|
-
* so the bash tool streams over it too.
|
|
6637
|
+
* `add` with `refresh` is what does the work; this is the guard in front of it, so an `update`
|
|
6638
|
+
* that finds nothing new neither stages nor publishes.
|
|
6733
6639
|
*/
|
|
6734
|
-
const
|
|
6735
|
-
const
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
if (options?.timeout === void 0) return widenExecError(run);
|
|
6745
|
-
const timeout = options.timeout;
|
|
6746
|
-
return run.pipe(Effect.timeoutOption(timeout), Effect.flatMap(Option.match({
|
|
6747
|
-
onNone: () => Effect.fail(new ToolShellTimeout({
|
|
6748
|
-
command,
|
|
6749
|
-
timeoutMillis: Duration.toMillis(timeout)
|
|
6750
|
-
})),
|
|
6751
|
-
onSome: Effect.succeed
|
|
6752
|
-
})));
|
|
6640
|
+
const update = Effect.fn("PluginStore.update")(function* (target, cache, options) {
|
|
6641
|
+
const current = yield* resolve$2(target, cache, options.from);
|
|
6642
|
+
if (current === void 0) return { _tag: "not-installed" };
|
|
6643
|
+
if ((yield* check(target, cache, {
|
|
6644
|
+
refresh: true,
|
|
6645
|
+
probe: options.probe,
|
|
6646
|
+
from: options.from
|
|
6647
|
+
}))._tag !== "outdated") return {
|
|
6648
|
+
_tag: "unchanged",
|
|
6649
|
+
entry: current
|
|
6753
6650
|
};
|
|
6754
|
-
const
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6651
|
+
const added = yield* add(target, cache, {
|
|
6652
|
+
refresh: true,
|
|
6653
|
+
validate: options.validate,
|
|
6654
|
+
from: options.from,
|
|
6655
|
+
...options.runner === void 0 ? {} : { runner: options.runner }
|
|
6656
|
+
});
|
|
6657
|
+
return added.entry.generation === current.generation ? {
|
|
6658
|
+
_tag: "unchanged",
|
|
6659
|
+
entry: current
|
|
6761
6660
|
} : {
|
|
6762
|
-
_tag: "
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6661
|
+
_tag: "updated",
|
|
6662
|
+
entry: added.entry,
|
|
6663
|
+
...added.validated === void 0 ? {} : { validated: added.validated }
|
|
6664
|
+
};
|
|
6665
|
+
});
|
|
6666
|
+
Effect.fn("PluginStore.remove")(function* (target, cache, from) {
|
|
6667
|
+
const { key, directory } = yield* entryDir(target, cache, from);
|
|
6668
|
+
const existed = yield* fileSystem.exists(directory).pipe(Effect.orElseSucceed(() => false));
|
|
6669
|
+
yield* fileSystem.remove(directory, {
|
|
6670
|
+
recursive: true,
|
|
6671
|
+
force: true
|
|
6672
|
+
}).pipe(Effect.ignore);
|
|
6673
|
+
yield* drop(root(cache), key);
|
|
6674
|
+
return existed;
|
|
6675
|
+
});
|
|
6676
|
+
Effect.fn("PluginStore.all")(function* (cache) {
|
|
6677
|
+
const base = root(cache);
|
|
6678
|
+
const slugs = yield* fileSystem.readDirectory(base).pipe(Effect.orElseSucceed(() => []));
|
|
6679
|
+
const found = [];
|
|
6680
|
+
for (const slug of slugs) {
|
|
6681
|
+
const digests = yield* fileSystem.readDirectory(hostPath.join(base, slug)).pipe(Effect.orElseSucceed(() => []));
|
|
6682
|
+
for (const key of digests) {
|
|
6683
|
+
const entry = yield* active(hostPath.join(base, slug, key), key);
|
|
6684
|
+
if (entry !== void 0) found.push(entry);
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
return found;
|
|
6688
|
+
});
|
|
6689
|
+
const Id = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*){2}$/));
|
|
6690
|
+
const Definition = Schema.Struct({
|
|
6691
|
+
id: Id,
|
|
6692
|
+
kind: Schema.Literals(Object.keys(plugin_exports$1.domains)),
|
|
6693
|
+
events: Schema.optional(Schema.Array(Schema.declare((value) => Predicate.hasProperty(value, "type") && Predicate.isString(value.type)))),
|
|
6694
|
+
setup: Schema.declare((value) => Predicate.isFunction(value))
|
|
6695
|
+
});
|
|
6696
|
+
/** Every plugin failure says which file to go and edit, when there is one. */
|
|
6697
|
+
const where = (origin) => origin.file === void 0 ? {} : { file: origin.file };
|
|
6698
|
+
const detail = (cause) => cause instanceof Error ? cause.message : typeof cause === "string" ? cause : String(cause);
|
|
6699
|
+
/** A reference that does not name anything loadable. */
|
|
6700
|
+
const sourceFailure = (origin, cause) => Schema.is(SourceError)(cause) ? cause : new SourceError({
|
|
6701
|
+
reason: "plugin-not-found",
|
|
6702
|
+
reference: origin.reference,
|
|
6703
|
+
...where(origin),
|
|
6704
|
+
message: detail(cause)
|
|
6705
|
+
});
|
|
6706
|
+
/** Node's own code for a failure, when it set one. */
|
|
6707
|
+
const codeOf = (cause) => Predicate.hasProperty(cause, "code") && Predicate.isString(cause.code) ? cause.code : void 0;
|
|
6781
6708
|
/**
|
|
6782
|
-
*
|
|
6783
|
-
* the handler depends only on {@link ToolShell} / {@link ToolProgress}, never
|
|
6784
|
-
* `sandbox/Shell`, so the backend (local OS / just-bash / remote provider) is
|
|
6785
|
-
* swapped by changing the provided Layer with no change to the tool.
|
|
6709
|
+
* A module that would not import.
|
|
6786
6710
|
*
|
|
6787
|
-
* Two
|
|
6788
|
-
*
|
|
6789
|
-
*
|
|
6790
|
-
*
|
|
6791
|
-
*
|
|
6792
|
-
*
|
|
6793
|
-
* produced so far is preserved.
|
|
6711
|
+
* Two of these get their own reason, because the remedy differs and that is the whole value of a
|
|
6712
|
+
* slug. A missing dependency means the plugin is fine and its `node_modules` is not, which a
|
|
6713
|
+
* person fixes in the package rather than in their settings. An uncompiled one means the plugin
|
|
6714
|
+
* shipped its sources: Node refuses to strip types under `node_modules`, and installing a plugin
|
|
6715
|
+
* runs no lifecycle scripts, so a `prepare` build never happens either -- the author has to ship
|
|
6716
|
+
* the build output, and nothing the user can do to their settings will help.
|
|
6794
6717
|
*
|
|
6795
|
-
*
|
|
6796
|
-
*
|
|
6797
|
-
*
|
|
6718
|
+
* `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` is matched by code rather than by message: it is
|
|
6719
|
+
* Node's documented identifier and exact, where the wording is free to change under us. Only an
|
|
6720
|
+
* installed plugin can reach it -- a local path lives outside `node_modules`, where Node strips
|
|
6721
|
+
* types happily, which is what keeps the single-file dev workflow working.
|
|
6798
6722
|
*/
|
|
6799
|
-
const
|
|
6800
|
-
|
|
6801
|
-
|
|
6723
|
+
const importFailure = (origin, cause) => new LoadError({
|
|
6724
|
+
reason: codeOf(cause) === "ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING" ? "plugin-not-compiled" : /cannot find (package|module)/i.test(detail(cause)) ? "plugin-missing-dependency" : "plugin-import-failed",
|
|
6725
|
+
reference: origin.reference,
|
|
6726
|
+
...where(origin),
|
|
6727
|
+
message: detail(cause),
|
|
6728
|
+
cause
|
|
6802
6729
|
});
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
exitCode: Schema.Finite
|
|
6730
|
+
/** A module that imported and is not a plugin. */
|
|
6731
|
+
const definitionFailure = (origin, cause, id) => new LoadError({
|
|
6732
|
+
reason: "plugin-invalid-definition",
|
|
6733
|
+
reference: origin.reference,
|
|
6734
|
+
...where(origin),
|
|
6735
|
+
message: detail(cause),
|
|
6736
|
+
...id === void 0 ? {} : { id },
|
|
6737
|
+
cause
|
|
6812
6738
|
});
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
/** Deadline exceeded — expected, model-visible. Carries the partial output produced so far. */
|
|
6819
|
-
var BashTimedOut = class extends Schema.TaggedError()("BashTimedOut", {
|
|
6820
|
-
...outputFields,
|
|
6821
|
-
timeoutSeconds: Schema.Finite
|
|
6822
|
-
}) {};
|
|
6823
|
-
const BashFailure = Schema.Union([BashFailed, BashTimedOut]);
|
|
6824
|
-
const bashDef = tool_exports.define({
|
|
6825
|
-
name: "bash",
|
|
6826
|
-
label: "bash",
|
|
6827
|
-
promptSnippet: "Execute bash commands (ls, grep, find, etc.).",
|
|
6828
|
-
description: `Execute a bash command in the working directory and return its combined stdout/stderr output. Output is truncated to the last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES}KB (whichever is hit first); when truncated, the full output is saved to a temp file. A non-zero exit code is reported as an error carrying the captured output.`,
|
|
6829
|
-
parameters: BashParams,
|
|
6830
|
-
success: BashSuccess,
|
|
6831
|
-
failure: BashFailure,
|
|
6832
|
-
encodeContent: (success) => [{
|
|
6833
|
-
type: "text",
|
|
6834
|
-
text: success.output
|
|
6835
|
-
}],
|
|
6836
|
-
encodeFailureContent: (failure) => [{
|
|
6837
|
-
type: "text",
|
|
6838
|
-
text: failure.output
|
|
6839
|
-
}]
|
|
6739
|
+
const validate = Effect.fn("PluginLoader.validate")(function* (input, origin, builtin = false) {
|
|
6740
|
+
yield* Schema.decodeUnknownEffect(Definition)(input).pipe(Effect.mapError((cause) => definitionFailure(origin, cause)));
|
|
6741
|
+
const plugin = input;
|
|
6742
|
+
if (!builtin && plugin.id.startsWith("codework.")) return yield* definitionFailure(origin, /* @__PURE__ */ new Error("codework namespace is reserved for builtins"), plugin.id);
|
|
6743
|
+
return plugin;
|
|
6840
6744
|
});
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6745
|
+
const Manifest = Schema.Struct({
|
|
6746
|
+
name: Schema.optional(Schema.String),
|
|
6747
|
+
exports: Schema.optional(Schema.Unknown)
|
|
6748
|
+
});
|
|
6749
|
+
const localUrl = Effect.fn("PluginLoader.localUrl")(function* (location, origin) {
|
|
6750
|
+
const escapes = (message) => new SourceError({
|
|
6751
|
+
reason: "plugin-escapes-root",
|
|
6752
|
+
reference: origin.reference,
|
|
6753
|
+
...where(origin),
|
|
6754
|
+
message
|
|
6755
|
+
});
|
|
6756
|
+
if ((yield* fileSystem.stat(location)).type !== "Directory") return { url: yield* Effect.try(() => resolveModule(location, hostPath.dirname(location))) };
|
|
6757
|
+
const manifestPath = hostPath.join(location, "package.json");
|
|
6758
|
+
if (!(yield* fileSystem.exists(manifestPath))) return { url: yield* Effect.try(() => resolveModule("./index", location)) };
|
|
6759
|
+
const manifest = yield* fileSystem.readFileString(manifestPath).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest))));
|
|
6760
|
+
if (manifest.exports !== void 0) {
|
|
6761
|
+
const name = manifest.name;
|
|
6762
|
+
if (!name) return yield* escapes("a local package with exports must declare its name");
|
|
6763
|
+
const url = yield* Effect.try(() => resolveModule(name, location));
|
|
6764
|
+
const resolved = yield* fileSystem.realPath(fileURLToPath(url));
|
|
6765
|
+
if (hostPath.relative(yield* fileSystem.realPath(location), resolved).startsWith("..")) return yield* escapes("package export escapes its root");
|
|
6766
|
+
return {
|
|
6767
|
+
url: pathToFileURL(resolved).href,
|
|
6768
|
+
name
|
|
6769
|
+
};
|
|
6854
6770
|
}
|
|
6855
|
-
|
|
6856
|
-
return
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6771
|
+
const url = yield* Effect.try(() => resolveModule(createRequire(pathToFileURL(manifestPath)).resolve(location), location));
|
|
6772
|
+
return manifest.name === void 0 ? { url } : {
|
|
6773
|
+
url,
|
|
6774
|
+
name: manifest.name
|
|
6775
|
+
};
|
|
6776
|
+
});
|
|
6777
|
+
/**
|
|
6778
|
+
* Install a package into the store, importing and validating it **while it is still staged**.
|
|
6779
|
+
*
|
|
6780
|
+
* The validation is passed down rather than done afterwards for one reason: the store publishes by
|
|
6781
|
+
* rename, so a package that turns out not to be a plugin must be rejected before it is published.
|
|
6782
|
+
* Publishing first would make the broken generation the newest marked one, which `resolve` returns
|
|
6783
|
+
* forever after -- and re-running `add` would hit the fast path and return it again.
|
|
6784
|
+
*/
|
|
6785
|
+
const install = Effect.fn("PluginLoader.install")(function* (target, cache, origin, options) {
|
|
6786
|
+
const added = yield* add(target, cache, {
|
|
6787
|
+
from: anchor$1(origin.file, options.hostDir),
|
|
6788
|
+
validate: (fetched) => imported(url(fetched.entrypoint), origin, options.import)
|
|
6789
|
+
});
|
|
6790
|
+
return {
|
|
6791
|
+
url: added.entry.url,
|
|
6792
|
+
...added.entry.version === void 0 ? {} : { version: added.entry.version },
|
|
6793
|
+
generation: added.entry.generation,
|
|
6794
|
+
...added.validated === void 0 ? {} : { plugin: added.validated }
|
|
6795
|
+
};
|
|
6796
|
+
});
|
|
6797
|
+
/**
|
|
6798
|
+
* The directory whose `.npmrc` chain names the artifact a reference resolves to: the project that
|
|
6799
|
+
* declared it. A settings file sits in `<root>/.codework/`, but npm reads a project `.npmrc` at the
|
|
6800
|
+
* package boundary discovered upward from the chain's start -- the root itself -- so anchoring at
|
|
6801
|
+
* the file's own directory would miss it. Absent a declaring file the host context stands in.
|
|
6802
|
+
*/
|
|
6803
|
+
const anchor$1 = (file, fallback) => {
|
|
6804
|
+
if (file === void 0) return fallback;
|
|
6805
|
+
const directory = hostPath.dirname(file);
|
|
6806
|
+
return hostPath.basename(directory) === ".codework" ? hostPath.dirname(directory) : directory;
|
|
6807
|
+
};
|
|
6808
|
+
/** The default installer, or the one the caller injected. */
|
|
6809
|
+
const installer = (target, origin, options) => options.install === void 0 ? install(target, options.cache, origin, options) : options.install(target, options.cache, anchor$1(origin.file, options.hostDir));
|
|
6810
|
+
/** Import a module and confirm it default-exports a plugin. */
|
|
6811
|
+
const imported = Effect.fn("PluginLoader.imported")(function* (url, origin, load) {
|
|
6812
|
+
const module = yield* Effect.tryPromise({
|
|
6813
|
+
try: () => (load ?? importModule)(url),
|
|
6814
|
+
catch: (cause) => importFailure(origin, cause)
|
|
6815
|
+
});
|
|
6816
|
+
return yield* validate(Predicate.isObject(module) && "default" in module ? module.default : void 0, origin);
|
|
6817
|
+
});
|
|
6818
|
+
/**
|
|
6819
|
+
* Import one entrypoint and confirm it is a plugin, without touching the store.
|
|
6820
|
+
*
|
|
6821
|
+
* This is what a caller hands the store as its `validate`: the artifact is checked while it is
|
|
6822
|
+
* still staged, so a package that turns out not to be a plugin is never published.
|
|
6823
|
+
*/
|
|
6824
|
+
const definition$1 = (entrypoint, reference) => imported(url(entrypoint), {
|
|
6825
|
+
index: 0,
|
|
6826
|
+
reference
|
|
6827
|
+
});
|
|
6828
|
+
/**
|
|
6829
|
+
* Install and import one reference, and report what it turned out to be, without selecting it.
|
|
6830
|
+
* `codework plugin add` uses this to prove a package really is a plugin before it writes the
|
|
6831
|
+
* reference into a settings file: the failure a user gets is the one they can act on — a bad
|
|
6832
|
+
* spec, an install that did not resolve, a module that exports no plugin — rather than a broken
|
|
6833
|
+
* configuration that only fails at the next run.
|
|
6834
|
+
*/
|
|
6835
|
+
const inspect = Effect.fn("PluginLoader.inspect")(function* (reference, options) {
|
|
6836
|
+
const origin = {
|
|
6837
|
+
index: 0,
|
|
6838
|
+
reference,
|
|
6839
|
+
file: options.file
|
|
6871
6840
|
};
|
|
6872
|
-
const
|
|
6841
|
+
const target = yield* parse$2(reference, options.hostDir);
|
|
6842
|
+
const loaded = yield* load$3(target, origin, options);
|
|
6873
6843
|
return {
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
...
|
|
6844
|
+
id: loaded.plugin.id,
|
|
6845
|
+
...loaded.version === void 0 ? {} : { version: loaded.version },
|
|
6846
|
+
...loaded.name === void 0 ? {} : { name: loaded.name }
|
|
6877
6847
|
};
|
|
6878
6848
|
});
|
|
6879
|
-
|
|
6880
|
-
const
|
|
6881
|
-
|
|
6882
|
-
output: snap.content,
|
|
6883
|
-
truncated: false
|
|
6884
|
-
};
|
|
6849
|
+
const load$3 = Effect.fn("PluginLoader.load")(function* (target, origin, options) {
|
|
6850
|
+
const installed = yield* target.kind === "local" ? localUrl(target.path, origin).pipe(Effect.mapError((cause) => sourceFailure(origin, cause))) : installer(target, origin, options).pipe(Effect.mapError((cause) => origin.file === void 0 ? cause : declaredIn(cause, origin.file)));
|
|
6851
|
+
const address = target.kind === "local" && options.reload !== void 0 && options.reload > 0 ? `${installed.url}?reload=${options.reload}` : installed.url;
|
|
6885
6852
|
return {
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
...
|
|
6853
|
+
plugin: installed.plugin ?? (yield* imported(address, origin, options.import)),
|
|
6854
|
+
source: origin.reference,
|
|
6855
|
+
..."version" in installed && installed.version !== void 0 ? { version: installed.version } : {},
|
|
6856
|
+
..."generation" in installed && installed.generation !== void 0 ? { generation: installed.generation } : {},
|
|
6857
|
+
...installed.name === void 0 ? {} : { name: installed.name }
|
|
6889
6858
|
};
|
|
6890
|
-
};
|
|
6891
|
-
/**
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6859
|
+
});
|
|
6860
|
+
/**
|
|
6861
|
+
* The version-free, location-anchored spelling of a reference.
|
|
6862
|
+
*
|
|
6863
|
+
* Two references naming the same module compare equal through it: `@acme/x@1.2.0` and `@acme/x`
|
|
6864
|
+
* are one package, and `./plugins/x.ts` is the file it resolves to from the directory that
|
|
6865
|
+
* declared it. Pure -- nothing is installed, imported or read.
|
|
6866
|
+
*/
|
|
6867
|
+
const canonical = (reference, hostDir) => Effect.map(parse$2(reference, hostDir), canonical$1);
|
|
6868
|
+
//#endregion
|
|
6869
|
+
//#region src/plugin/catalog.ts
|
|
6870
|
+
/**
|
|
6871
|
+
* A definition carries `setup`; a patch carries `plugin` or `package` and no `setup`. Checking
|
|
6872
|
+
* `setup` first matters because `Plugin` permits extra properties, so a definition holding its
|
|
6873
|
+
* own `plugin` property must not be read as configuration.
|
|
6874
|
+
*
|
|
6875
|
+
* e.g
|
|
6876
|
+
* "acme/tool@1.2.0" → module (load me)
|
|
6877
|
+
* define({id, kind, setup, ...}) → module (I AM the definition — embedder code only)
|
|
6878
|
+
* { plugin: "x", enabled: false } → config (don't load — adjust something already loaded)
|
|
6879
|
+
*
|
|
6880
|
+
* setup present → definition, always (even with a stray plugin: key)
|
|
6881
|
+
* setup absent + plugin:/package: present → config patch
|
|
6882
|
+
* setup absent + neither key → malformed
|
|
6883
|
+
*/
|
|
6884
|
+
const isPatch = (reference) => Predicate.isObject(reference) && !Predicate.isFunction(reference) && !("setup" in reference) && ("plugin" in reference || "package" in reference);
|
|
6885
|
+
/**
|
|
6886
|
+
* The **load pass**: store lookup and ESM import, never the network.
|
|
6887
|
+
*
|
|
6888
|
+
* Runs at boot, at `reload`, and at any exchange that finds a reference it cannot satisfy. It
|
|
6889
|
+
* deliberately knows nothing about `enabled`, options or order -- those are data over modules that
|
|
6890
|
+
* are already loaded, they cost a walk of a short array, and they belong to every exchange.
|
|
6891
|
+
*
|
|
6892
|
+
* Loading is keyed by what a reference resolves to, so naming one module twice imports it once.
|
|
6893
|
+
*/
|
|
6894
|
+
const load$2 = Effect.fn("PluginCatalog.load")(function* (references, options) {
|
|
6895
|
+
const plugins = /* @__PURE__ */ new Map();
|
|
6896
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
6897
|
+
const versions = /* @__PURE__ */ new Map();
|
|
6898
|
+
const origins = /* @__PURE__ */ new Map();
|
|
6899
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6900
|
+
const remember = (plugin, names, from) => {
|
|
6901
|
+
plugins.set(plugin.id, plugin);
|
|
6902
|
+
aliases.set(plugin.id, plugin.id);
|
|
6903
|
+
for (const name of names) aliases.set(name, plugin.id);
|
|
6904
|
+
if (from?.version !== void 0) versions.set(plugin.id, from.version);
|
|
6905
|
+
if (from !== void 0) origins.set(plugin.id, from.origin);
|
|
6906
|
+
};
|
|
6907
|
+
/** Source metadata exists for diagnostics; a silent ID replacement is where it earns that. */
|
|
6908
|
+
const note = (plugin, source) => plugins.has(plugin.id) ? Effect.logDebug(`plugin ${plugin.id} redefined by ${source}; the earlier definition is discarded`) : Effect.void;
|
|
6909
|
+
for (const builtin of options.builtins) remember(yield* validate(builtin, {
|
|
6910
|
+
index: -1,
|
|
6911
|
+
reference: builtin.id
|
|
6912
|
+
}, true), []);
|
|
6913
|
+
for (const [index, reference] of references.entries()) {
|
|
6914
|
+
if (isPatch(reference)) continue;
|
|
6915
|
+
const declared = typeof reference === "string" ? reference : Predicate.hasProperty(reference, "id") ? reference.id : void 0;
|
|
6916
|
+
const named = typeof declared === "string" ? declared : `<plugin object #${index}>`;
|
|
6917
|
+
const origin = {
|
|
6918
|
+
index,
|
|
6919
|
+
reference: named,
|
|
6920
|
+
file: options.declared?.get(named)
|
|
6921
|
+
};
|
|
6922
|
+
if (typeof reference !== "string") {
|
|
6923
|
+
if (typeof declared === "string" && plugins.get(declared) === reference) continue;
|
|
6924
|
+
const plugin = yield* validate(reference, origin);
|
|
6925
|
+
yield* note(plugin, "a supplied object");
|
|
6926
|
+
remember(plugin, []);
|
|
6927
|
+
continue;
|
|
6928
|
+
}
|
|
6929
|
+
const target = yield* parse$2(reference, options.hostDir);
|
|
6930
|
+
const key = target.kind === "local" ? target.path : target.spec;
|
|
6931
|
+
const already = seen.get(key);
|
|
6932
|
+
if (already !== void 0) {
|
|
6933
|
+
aliases.set(reference, already);
|
|
6934
|
+
continue;
|
|
6935
|
+
}
|
|
6936
|
+
const definition = yield* load$3(target, origin, options);
|
|
6937
|
+
yield* note(definition.plugin, definition.version === void 0 ? key : `${key}@${definition.version}`);
|
|
6938
|
+
const names = target.kind === "local" ? [target.path, ...definition.name === void 0 ? [] : [definition.name]] : [canonical$1(target), target.spec];
|
|
6939
|
+
remember(definition.plugin, [reference, ...names], {
|
|
6940
|
+
...definition.version === void 0 ? {} : { version: definition.version },
|
|
6941
|
+
origin: {
|
|
6942
|
+
reference,
|
|
6943
|
+
...definition.generation === void 0 ? {} : { generation: definition.generation },
|
|
6944
|
+
...origin.file === void 0 ? {} : { file: origin.file }
|
|
6945
|
+
}
|
|
6946
|
+
});
|
|
6947
|
+
seen.set(key, definition.plugin.id);
|
|
6948
|
+
}
|
|
6906
6949
|
return {
|
|
6907
|
-
|
|
6908
|
-
|
|
6950
|
+
plugins,
|
|
6951
|
+
aliases,
|
|
6952
|
+
versions,
|
|
6953
|
+
origins
|
|
6909
6954
|
};
|
|
6910
6955
|
});
|
|
6911
|
-
/**
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
...present
|
|
6933
|
-
});
|
|
6934
|
-
const code = yield* Ref.get(exitCode);
|
|
6935
|
-
if (code === null || code !== 0) return yield* new BashFailed({
|
|
6936
|
-
exitCode: code ?? -1,
|
|
6937
|
-
...present
|
|
6956
|
+
/**
|
|
6957
|
+
* The **config pass**: pure data over modules that are already loaded, run at every exchange.
|
|
6958
|
+
*
|
|
6959
|
+
* An array entry is a **module** — a definition object, a path, a `file:` URL, or a package
|
|
6960
|
+
* spec. It takes the position it is written at, so naming a module again moves it.
|
|
6961
|
+
*
|
|
6962
|
+
* An object entry is **configuration** for a plugin an earlier entry (or the built-in list)
|
|
6963
|
+
* already selected, addressed by its ID (`plugin`) or the module string (`package`) that selected
|
|
6964
|
+
* it. It never loads, installs or reorders anything, and a name matching nothing in the selection
|
|
6965
|
+
* is ignored rather than failing the boot — a typo costs a debug line, never a fetch.
|
|
6966
|
+
*
|
|
6967
|
+
* Nothing here touches the disk, which is the whole point of the split: an `options` edit, an
|
|
6968
|
+
* `enabled` flip or a reordering applies at the next exchange without importing anything.
|
|
6969
|
+
*/
|
|
6970
|
+
const select = Effect.fn("PluginCatalog.select")(function* (references, pool) {
|
|
6971
|
+
const operations = /* @__PURE__ */ new Map();
|
|
6972
|
+
const missing = [];
|
|
6973
|
+
const choose = (id, index) => operations.set(id, {
|
|
6974
|
+
enabled: true,
|
|
6975
|
+
index,
|
|
6976
|
+
options: operations.get(id)?.options ?? {}
|
|
6938
6977
|
});
|
|
6978
|
+
for (const [index, reference] of references.entries()) {
|
|
6979
|
+
if (isPatch(reference)) {
|
|
6980
|
+
const named = reference.plugin ?? reference.package;
|
|
6981
|
+
const origin = {
|
|
6982
|
+
index,
|
|
6983
|
+
reference: typeof named === "string" ? named : `<plugin entry #${index}>`
|
|
6984
|
+
};
|
|
6985
|
+
const invalid = reference.plugin !== void 0 && reference.package !== void 0 ? "names both `plugin` and `package`" : typeof named !== "string" || named.length === 0 ? "names neither `plugin` nor `package`" : reference.enabled !== void 0 && typeof reference.enabled !== "boolean" ? "`enabled` is not a boolean" : reference.options !== void 0 && !isRecord$1(reference.options) ? "`options` is not a plain object" : void 0;
|
|
6986
|
+
if (invalid !== void 0) return yield* definitionFailure(origin, /* @__PURE__ */ new Error(`plugin entry ${invalid}`));
|
|
6987
|
+
const id = reference.plugin === void 0 ? pool.aliases.get(reference.package) : reference.plugin;
|
|
6988
|
+
const operation = id === void 0 ? void 0 : operations.get(id);
|
|
6989
|
+
if (id === void 0 || operation === void 0) {
|
|
6990
|
+
yield* Effect.logDebug(`plugin configuration ignored: nothing selected under ${named}`);
|
|
6991
|
+
continue;
|
|
6992
|
+
}
|
|
6993
|
+
operations.set(id, {
|
|
6994
|
+
enabled: reference.enabled ?? operation.enabled,
|
|
6995
|
+
index: operation.index,
|
|
6996
|
+
options: reference.options ?? operation.options
|
|
6997
|
+
});
|
|
6998
|
+
continue;
|
|
6999
|
+
}
|
|
7000
|
+
if (typeof reference !== "string") {
|
|
7001
|
+
const declared = Predicate.hasProperty(reference, "id") ? reference.id : void 0;
|
|
7002
|
+
if (typeof declared === "string" && pool.plugins.has(declared)) choose(declared, index);
|
|
7003
|
+
continue;
|
|
7004
|
+
}
|
|
7005
|
+
const id = pool.aliases.get(reference);
|
|
7006
|
+
if (id === void 0) missing.push(reference);
|
|
7007
|
+
else choose(id, index);
|
|
7008
|
+
}
|
|
7009
|
+
const selection = [];
|
|
7010
|
+
for (const [id, operation] of operations) {
|
|
7011
|
+
if (!operation.enabled) continue;
|
|
7012
|
+
const plugin = pool.plugins.get(id);
|
|
7013
|
+
if (plugin !== void 0) selection.push({
|
|
7014
|
+
plugin,
|
|
7015
|
+
options: Object.freeze(operation.options),
|
|
7016
|
+
index: operation.index
|
|
7017
|
+
});
|
|
7018
|
+
}
|
|
7019
|
+
selection.sort((a, b) => (0, plugin_exports$1.rank)(a.plugin.kind) - (0, plugin_exports$1.rank)(b.plugin.kind) || a.index - b.index);
|
|
6939
7020
|
return {
|
|
6940
|
-
|
|
6941
|
-
|
|
7021
|
+
selection: Object.freeze(selection.map(({ plugin, options }) => ({
|
|
7022
|
+
plugin,
|
|
7023
|
+
options
|
|
7024
|
+
}))),
|
|
7025
|
+
missing
|
|
6942
7026
|
};
|
|
6943
|
-
}));
|
|
6944
|
-
const bashHandler = (params) => Effect.gen(function* () {
|
|
6945
|
-
const shell = yield* ToolShell;
|
|
6946
|
-
return shell.stream !== void 0 ? yield* runStreaming(shell.stream, params) : yield* runBuffered(shell, params);
|
|
6947
7027
|
});
|
|
6948
|
-
/** The bash tool: definition + handler, wired the testable (def/exec split) way. */
|
|
6949
|
-
const bashTool = tool_exports.implement(bashDef, bashHandler);
|
|
6950
|
-
const bashPlugin = (0, plugin_exports$1.define)({
|
|
6951
|
-
id: "codework.tool.bash",
|
|
6952
|
-
kind: "tool",
|
|
6953
|
-
setup: Effect.fn("BashPlugin.setup")(function* (ctx) {
|
|
6954
|
-
const shell = yield* io_exports.SandboxIO.Shell;
|
|
6955
|
-
const mounted = fromSandboxShell.pipe(Layer.provide(Layer.succeed(io_exports.SandboxIO.Shell, shell)));
|
|
6956
|
-
ctx.plugin.tools.add(tool_exports.provide(bashTool, mounted));
|
|
6957
|
-
})
|
|
6958
|
-
});
|
|
6959
|
-
//#endregion
|
|
6960
|
-
//#region src/plugin/builtin.ts
|
|
6961
7028
|
/**
|
|
6962
|
-
*
|
|
6963
|
-
*
|
|
7029
|
+
* The exchange-boundary check: has the store moved under this pool?
|
|
7030
|
+
*
|
|
7031
|
+
* Every exchange already re-reads every settings layer from disk, and the config pass already
|
|
7032
|
+
* walks those entries against the loaded set. Two things can be settled right there, and they are
|
|
7033
|
+
* the same question asked twice:
|
|
7034
|
+
*
|
|
7035
|
+
* 1. a settings entry names a module that is not loaded, and the store holds it;
|
|
7036
|
+
* 2. a loaded module is not the newest generation the store has.
|
|
7037
|
+
*
|
|
7038
|
+
* The second is what makes `plugin update` finish. Without it the question is "is this entry
|
|
7039
|
+
* loaded?", the answer after an update is still yes, and the bytes just fetched sit on disk until
|
|
7040
|
+
* something else forces an import.
|
|
7041
|
+
*
|
|
7042
|
+
* Returns the new pool, or nothing when nothing moved -- which is the common case, and costs one
|
|
7043
|
+
* store lookup per filed module.
|
|
7044
|
+
*
|
|
7045
|
+
* **This is not a watcher.** Nothing polls and no fiber exists; it is the observation that we were
|
|
7046
|
+
* already paying for the I/O that would tell us, and discarding the answer. Three bounds keep it
|
|
7047
|
+
* safe: the store only and never the network, so an exchange cannot block on a registry;
|
|
7048
|
+
* resolve-only, so it cannot install; and once, at the top of a snapshot, so an exchange already
|
|
7049
|
+
* running is unaffected.
|
|
6964
7050
|
*/
|
|
6965
|
-
const
|
|
7051
|
+
const follow = Effect.fn("PluginCatalog.follow")(function* (references, pool, options, filed) {
|
|
7052
|
+
let moved = false;
|
|
7053
|
+
for (const reference of references) {
|
|
7054
|
+
if (typeof reference !== "string" || pool.aliases.has(reference)) continue;
|
|
7055
|
+
if (Option.isSome(yield* filed(reference, anchor$1(options.declared?.get(reference), options.hostDir)))) {
|
|
7056
|
+
moved = true;
|
|
7057
|
+
break;
|
|
7058
|
+
}
|
|
7059
|
+
}
|
|
7060
|
+
if (!moved) for (const origin of pool.origins.values()) {
|
|
7061
|
+
if (origin.generation === void 0) continue;
|
|
7062
|
+
const current = yield* filed(origin.reference, anchor$1(origin.file, options.hostDir));
|
|
7063
|
+
if (Option.isSome(current) && current.value.generation !== void 0 && current.value.generation > origin.generation) {
|
|
7064
|
+
moved = true;
|
|
7065
|
+
break;
|
|
7066
|
+
}
|
|
7067
|
+
}
|
|
7068
|
+
if (!moved) return Option.none();
|
|
7069
|
+
const retained = [];
|
|
7070
|
+
for (const origin of pool.origins.values()) {
|
|
7071
|
+
if (references.includes(origin.reference) || Option.isSome(yield* filed(origin.reference, anchor$1(origin.file, options.hostDir)))) {
|
|
7072
|
+
retained.push(origin);
|
|
7073
|
+
continue;
|
|
7074
|
+
}
|
|
7075
|
+
yield* Effect.logWarning(`plugin ${origin.reference} no longer resolves; it is dropped from the loaded set`);
|
|
7076
|
+
}
|
|
7077
|
+
const accumulated = [...Array.from(retained, (origin) => origin.reference), ...references];
|
|
7078
|
+
const known = /* @__PURE__ */ new Map();
|
|
7079
|
+
for (const origin of retained) if (origin.file !== void 0) known.set(origin.reference, origin.file);
|
|
7080
|
+
for (const [reference, file] of options.declared ?? /* @__PURE__ */ new Map()) known.set(reference, file);
|
|
7081
|
+
return Option.some(yield* load$2(accumulated, {
|
|
7082
|
+
...options,
|
|
7083
|
+
declared: known
|
|
7084
|
+
}));
|
|
7085
|
+
});
|
|
7086
|
+
Effect.fn("PluginCatalog.prepare")(function* (references, options) {
|
|
7087
|
+
const pool = yield* load$2(references, options);
|
|
7088
|
+
return (yield* select(references, pool)).selection;
|
|
7089
|
+
});
|
|
6966
7090
|
//#endregion
|
|
6967
7091
|
//#region src/runner/coordinator.ts
|
|
6968
7092
|
const make$7 = (options) => Effect.gen(function* () {
|
|
@@ -7104,6 +7228,82 @@ const layer$7 = Layer.effect(Service$6, Effect.gen(function* () {
|
|
|
7104
7228
|
});
|
|
7105
7229
|
}));
|
|
7106
7230
|
//#endregion
|
|
7231
|
+
//#region src/settings/schema.ts
|
|
7232
|
+
var schema_exports$3 = /* @__PURE__ */ __exportAll({});
|
|
7233
|
+
import * as import__codeworksh_plugin_settings from "@codeworksh/plugin/settings";
|
|
7234
|
+
__reExport(schema_exports$3, import__codeworksh_plugin_settings);
|
|
7235
|
+
//#endregion
|
|
7236
|
+
//#region src/settings/resolve.ts
|
|
7237
|
+
const patternMatches = (pattern, id) => new RegExp(`^${pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(id);
|
|
7238
|
+
function collect(settings, provider, id) {
|
|
7239
|
+
const config = settings.model;
|
|
7240
|
+
const blocks = config.providerOptions?.[provider] ?? {};
|
|
7241
|
+
const patterns = Object.keys(blocks).filter((key) => key !== "*" && key !== id && key.includes("*") && patternMatches(key, id)).sort((a, b) => a.indexOf("*") - b.indexOf("*"));
|
|
7242
|
+
return merge({
|
|
7243
|
+
thinkingLevel: config.thinkingLevel,
|
|
7244
|
+
toolExecution: config.toolExecution,
|
|
7245
|
+
...config.thinkingBudgets === void 0 ? {} : { thinkingBudgets: config.thinkingBudgets }
|
|
7246
|
+
}, config.options, blocks["*"], ...patterns.map((key) => blocks[key]), blocks[id]);
|
|
7247
|
+
}
|
|
7248
|
+
/**
|
|
7249
|
+
* Corrections to the catalog entry itself, applied before lookup.
|
|
7250
|
+
*
|
|
7251
|
+
* `protocol` is here rather than in the request bag because it is model shape: it decides which
|
|
7252
|
+
* wire protocol aikit speaks, and through `Model.optionsKey` which namespace the provider bag is
|
|
7253
|
+
* keyed by. A catalog cannot know it for a local OpenAI-compatible server, and the person
|
|
7254
|
+
* pointing at one does.
|
|
7255
|
+
*/
|
|
7256
|
+
const resolveOverrides = (block) => ({
|
|
7257
|
+
...block.contextWindow === void 0 ? {} : { contextWindow: block.contextWindow },
|
|
7258
|
+
...block.protocol === void 0 ? {} : { protocol: block.protocol }
|
|
7259
|
+
});
|
|
7260
|
+
/** Protocol-independent request attributes can already be captured by State. */
|
|
7261
|
+
function resolveOptions(block) {
|
|
7262
|
+
return {
|
|
7263
|
+
...Object.fromEntries(Object.entries(block).filter(([key]) => Object.hasOwn(schema_exports$3.requestFields, key))),
|
|
7264
|
+
...block.extras === void 0 ? {} : { factoryOptions: merge({}, block.extras) }
|
|
7265
|
+
};
|
|
7266
|
+
}
|
|
7267
|
+
/** The bag key becomes knowable only after lookup at the LLM boundary. */
|
|
7268
|
+
function resolveRequest(block, model) {
|
|
7269
|
+
const bag = Object.fromEntries(Object.entries(block).filter(([key]) => !Object.hasOwn(schema_exports$3.requestFields, key) && ![
|
|
7270
|
+
"thinkingLevel",
|
|
7271
|
+
"toolExecution",
|
|
7272
|
+
"contextWindow",
|
|
7273
|
+
"protocol",
|
|
7274
|
+
"extras"
|
|
7275
|
+
].includes(key)));
|
|
7276
|
+
return {
|
|
7277
|
+
...resolveOptions(block),
|
|
7278
|
+
...Object.keys(bag).length === 0 ? {} : { providerOptions: { [Model$1.optionsKey(model)]: bag } }
|
|
7279
|
+
};
|
|
7280
|
+
}
|
|
7281
|
+
/**
|
|
7282
|
+
* Harness-owned controls, which never reach a provider.
|
|
7283
|
+
*
|
|
7284
|
+
* Separate from {@link resolveRequest} because these steer the loop itself -- how hard to
|
|
7285
|
+
* think, how to schedule tool calls -- rather than the request body. Runtime bindings
|
|
7286
|
+
* outrank a matched block, which outranks the file's top level.
|
|
7287
|
+
*/
|
|
7288
|
+
const resolveControls = (settings, block, runtime) => ({
|
|
7289
|
+
thinkingLevel: runtime.thinkingLevel ?? block.thinkingLevel ?? settings.model.thinkingLevel,
|
|
7290
|
+
toolExecution: runtime.toolExecution ?? block.toolExecution ?? settings.model.toolExecution
|
|
7291
|
+
});
|
|
7292
|
+
/** Pure configuration composition. Domain selections need not exist in the catalog. */
|
|
7293
|
+
function compose(settings, options = {}, session = {}) {
|
|
7294
|
+
const runtime = merge(options, session);
|
|
7295
|
+
const provider = runtime.provider ?? settings.model.provider;
|
|
7296
|
+
const model = runtime.model ?? settings.model.id;
|
|
7297
|
+
const block = collect(settings, provider, model);
|
|
7298
|
+
return {
|
|
7299
|
+
provider,
|
|
7300
|
+
model,
|
|
7301
|
+
...resolveControls(settings, block, runtime),
|
|
7302
|
+
block,
|
|
7303
|
+
runtime
|
|
7304
|
+
};
|
|
7305
|
+
}
|
|
7306
|
+
//#endregion
|
|
7107
7307
|
//#region src/runner/event.ts
|
|
7108
7308
|
const make$6 = Effect.fn("LLMEventPublisher.make")(function* (input) {
|
|
7109
7309
|
const events = yield* Service$16;
|
|
@@ -7253,82 +7453,6 @@ const make$6 = Effect.fn("LLMEventPublisher.make")(function* (input) {
|
|
|
7253
7453
|
};
|
|
7254
7454
|
});
|
|
7255
7455
|
//#endregion
|
|
7256
|
-
//#region src/settings/schema.ts
|
|
7257
|
-
var schema_exports$3 = /* @__PURE__ */ __exportAll({});
|
|
7258
|
-
import * as import__codeworksh_plugin_settings from "@codeworksh/plugin/settings";
|
|
7259
|
-
__reExport(schema_exports$3, import__codeworksh_plugin_settings);
|
|
7260
|
-
//#endregion
|
|
7261
|
-
//#region src/settings/resolve.ts
|
|
7262
|
-
const patternMatches = (pattern, id) => new RegExp(`^${pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(id);
|
|
7263
|
-
function collect(settings, provider, id) {
|
|
7264
|
-
const config = settings.model;
|
|
7265
|
-
const blocks = config.providerOptions?.[provider] ?? {};
|
|
7266
|
-
const patterns = Object.keys(blocks).filter((key) => key !== "*" && key !== id && key.includes("*") && patternMatches(key, id)).sort((a, b) => a.indexOf("*") - b.indexOf("*"));
|
|
7267
|
-
return merge({
|
|
7268
|
-
thinkingLevel: config.thinkingLevel,
|
|
7269
|
-
toolExecution: config.toolExecution,
|
|
7270
|
-
...config.thinkingBudgets === void 0 ? {} : { thinkingBudgets: config.thinkingBudgets }
|
|
7271
|
-
}, config.options, blocks["*"], ...patterns.map((key) => blocks[key]), blocks[id]);
|
|
7272
|
-
}
|
|
7273
|
-
/**
|
|
7274
|
-
* Corrections to the catalog entry itself, applied before lookup.
|
|
7275
|
-
*
|
|
7276
|
-
* `protocol` is here rather than in the request bag because it is model shape: it decides which
|
|
7277
|
-
* wire protocol aikit speaks, and through `Model.optionsKey` which namespace the provider bag is
|
|
7278
|
-
* keyed by. A catalog cannot know it for a local OpenAI-compatible server, and the person
|
|
7279
|
-
* pointing at one does.
|
|
7280
|
-
*/
|
|
7281
|
-
const resolveOverrides = (block) => ({
|
|
7282
|
-
...block.contextWindow === void 0 ? {} : { contextWindow: block.contextWindow },
|
|
7283
|
-
...block.protocol === void 0 ? {} : { protocol: block.protocol }
|
|
7284
|
-
});
|
|
7285
|
-
/** Protocol-independent request attributes can already be captured by State. */
|
|
7286
|
-
function resolveOptions(block) {
|
|
7287
|
-
return {
|
|
7288
|
-
...Object.fromEntries(Object.entries(block).filter(([key]) => Object.hasOwn(schema_exports$3.requestFields, key))),
|
|
7289
|
-
...block.extras === void 0 ? {} : { factoryOptions: merge({}, block.extras) }
|
|
7290
|
-
};
|
|
7291
|
-
}
|
|
7292
|
-
/** The bag key becomes knowable only after lookup at the LLM boundary. */
|
|
7293
|
-
function resolveRequest(block, model) {
|
|
7294
|
-
const bag = Object.fromEntries(Object.entries(block).filter(([key]) => !Object.hasOwn(schema_exports$3.requestFields, key) && ![
|
|
7295
|
-
"thinkingLevel",
|
|
7296
|
-
"toolExecution",
|
|
7297
|
-
"contextWindow",
|
|
7298
|
-
"protocol",
|
|
7299
|
-
"extras"
|
|
7300
|
-
].includes(key)));
|
|
7301
|
-
return {
|
|
7302
|
-
...resolveOptions(block),
|
|
7303
|
-
...Object.keys(bag).length === 0 ? {} : { providerOptions: { [Model$1.optionsKey(model)]: bag } }
|
|
7304
|
-
};
|
|
7305
|
-
}
|
|
7306
|
-
/**
|
|
7307
|
-
* Harness-owned controls, which never reach a provider.
|
|
7308
|
-
*
|
|
7309
|
-
* Separate from {@link resolveRequest} because these steer the loop itself -- how hard to
|
|
7310
|
-
* think, how to schedule tool calls -- rather than the request body. Runtime bindings
|
|
7311
|
-
* outrank a matched block, which outranks the file's top level.
|
|
7312
|
-
*/
|
|
7313
|
-
const resolveControls = (settings, block, runtime) => ({
|
|
7314
|
-
thinkingLevel: runtime.thinkingLevel ?? block.thinkingLevel ?? settings.model.thinkingLevel,
|
|
7315
|
-
toolExecution: runtime.toolExecution ?? block.toolExecution ?? settings.model.toolExecution
|
|
7316
|
-
});
|
|
7317
|
-
/** Pure configuration composition. Domain selections need not exist in the catalog. */
|
|
7318
|
-
function compose(settings, options = {}, session = {}) {
|
|
7319
|
-
const runtime = merge(options, session);
|
|
7320
|
-
const provider = runtime.provider ?? settings.model.provider;
|
|
7321
|
-
const model = runtime.model ?? settings.model.id;
|
|
7322
|
-
const block = collect(settings, provider, model);
|
|
7323
|
-
return {
|
|
7324
|
-
provider,
|
|
7325
|
-
model,
|
|
7326
|
-
...resolveControls(settings, block, runtime),
|
|
7327
|
-
block,
|
|
7328
|
-
runtime
|
|
7329
|
-
};
|
|
7330
|
-
}
|
|
7331
|
-
//#endregion
|
|
7332
7456
|
//#region src/runner/llm.ts
|
|
7333
7457
|
const runtimeOptions = (input, model, signal) => {
|
|
7334
7458
|
const { activeTools, ...options } = merge(resolveRequest(input.settings ?? {}, model), input.options);
|
|
@@ -7378,17 +7502,6 @@ const providerError = (input, failure) => {
|
|
|
7378
7502
|
});
|
|
7379
7503
|
};
|
|
7380
7504
|
const providerErrorFromUnknown = (input, cause) => providerError(input, AikitFailure.normalize(cause));
|
|
7381
|
-
const modelCatalogFailure = (cause) => {
|
|
7382
|
-
if (typeof cause !== "object" || cause === null || !("name" in cause) || cause.name !== "ModelCatalogLoadError") return;
|
|
7383
|
-
if (!("data" in cause) || typeof cause.data !== "object" || cause.data === null) return void 0;
|
|
7384
|
-
const data = cause.data;
|
|
7385
|
-
if (!("path" in data) || typeof data.path !== "string" || !("message" in data) || typeof data.message !== "string" || !("reason" in data) || data.reason !== "missing" && data.reason !== "unreadable" && data.reason !== "empty" && data.reason !== "invalid") return;
|
|
7386
|
-
return {
|
|
7387
|
-
path: data.path,
|
|
7388
|
-
reason: data.reason,
|
|
7389
|
-
message: data.message
|
|
7390
|
-
};
|
|
7391
|
-
};
|
|
7392
7505
|
/** Read structured terminal data while remaining compatible with older Aikit messages. */
|
|
7393
7506
|
const messageFailure = (message) => {
|
|
7394
7507
|
const candidate = message.failure;
|
|
@@ -7398,14 +7511,7 @@ const messageFailure = (message) => {
|
|
|
7398
7511
|
const resolve$1 = Effect.fn("LLM.resolve")(function* (input) {
|
|
7399
7512
|
const model = yield* Effect.tryPromise({
|
|
7400
7513
|
try: () => llm(input.provider, input.model, resolveOverrides(input.settings ?? {})),
|
|
7401
|
-
catch: (cause) =>
|
|
7402
|
-
const catalog = modelCatalogFailure(cause);
|
|
7403
|
-
return catalog === void 0 ? providerErrorFromUnknown(input, cause) : new ModelCatalogError({
|
|
7404
|
-
path: catalog.path,
|
|
7405
|
-
reason: catalog.reason,
|
|
7406
|
-
detail: catalog.message
|
|
7407
|
-
});
|
|
7408
|
-
}
|
|
7514
|
+
catch: (cause) => loadError(cause) ?? providerErrorFromUnknown(input, cause)
|
|
7409
7515
|
});
|
|
7410
7516
|
if (model === void 0) return yield* new ModelNotFoundError({
|
|
7411
7517
|
provider: input.provider,
|
|
@@ -8086,7 +8192,7 @@ const parse$1 = Effect.fn("Settings.parse")(function* (path, source) {
|
|
|
8086
8192
|
reason: "parse",
|
|
8087
8193
|
detail: `${printParseErrorCode(first.error)} at ${at(source, first.offset)}`
|
|
8088
8194
|
});
|
|
8089
|
-
const document = normalize
|
|
8195
|
+
const document = normalize(json);
|
|
8090
8196
|
if (Predicate.isObject(document) && Predicate.isObject(json) && "plugins" in json) Object.assign(document, { plugins: json.plugins });
|
|
8091
8197
|
return yield* Schema.decodeUnknownEffect(schema_exports$3.Patch)(document).pipe(Effect.mapError((error) => new SettingsError({
|
|
8092
8198
|
path,
|
|
@@ -8769,7 +8875,7 @@ const layer$2 = Layer.effectDiscard(Effect.gen(function* () {
|
|
|
8769
8875
|
}));
|
|
8770
8876
|
//#endregion
|
|
8771
8877
|
//#region src/session/live.ts
|
|
8772
|
-
const layer$1 = Layer.provideMerge(layer$2, layer$
|
|
8878
|
+
const layer$1 = Layer.provideMerge(layer$2, layer$23);
|
|
8773
8879
|
//#endregion
|
|
8774
8880
|
//#region src/effect/harness.ts
|
|
8775
8881
|
var harness_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -8834,14 +8940,17 @@ const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
|
|
|
8834
8940
|
});
|
|
8835
8941
|
});
|
|
8836
8942
|
const definitions = yield* flatten([...(yield* Ref.get(pool)).plugins.values()]);
|
|
8837
|
-
const database = layer$
|
|
8943
|
+
const database = layer$26(resolveDatabaseLocation(options.database ?? (yield* locationConfig), paths.data));
|
|
8838
8944
|
const configured = yield* loadAll(options.sandboxes ?? [], { hostCwd });
|
|
8839
|
-
const drivers = layer$
|
|
8840
|
-
const sandboxes = layer$
|
|
8945
|
+
const drivers = layer$19(driver_exports.SandboxDriver.withSource(make$11().driver, "core"), driver_exports.SandboxDriver.withSource(make$9().driver, "core"), ...configured);
|
|
8946
|
+
const sandboxes = layer$17({ hostCwd }).pipe(Layer.provideMerge(drivers), Layer.provideMerge(database));
|
|
8841
8947
|
const authFile$1 = authFile(options.home === void 0 ? void 0 : paths.home);
|
|
8842
8948
|
const open = options.llm ?? openWith(authFile$1 === void 0 ? {} : { authFile: authFile$1 });
|
|
8843
8949
|
const loop = layer$3({ request: make$5(open) });
|
|
8844
|
-
return layer$
|
|
8950
|
+
return layer$10.pipe(Layer.provideMerge(layer$7.pipe(Layer.provide(loop))), Layer.provideMerge(layer$4({}, pool, references, followStore, rebuild)), Layer.provideMerge(layer$5(settingsOptions)), Layer.provideMerge(layer$6), Layer.provideMerge(sandboxes), Layer.provideMerge(layer$22), Layer.provideMerge(layer$1), Layer.provideMerge(layer$25), Layer.provideMerge(layer$9(definitions)), Layer.provideMerge(database), Layer.provideMerge(global), Layer.provideMerge(layer$8({
|
|
8951
|
+
home: paths.home,
|
|
8952
|
+
watch: options.watchModels ?? false
|
|
8953
|
+
})));
|
|
8845
8954
|
}));
|
|
8846
8955
|
//#endregion
|
|
8847
8956
|
//#region src/effect/sandbox.ts
|
|
@@ -9149,6 +9258,6 @@ var DateTimeUtcFromMillis = schema_exports$9.DateTimeUtcFromMillis;
|
|
|
9149
9258
|
var EventSchema = schema_exports$7.EventSchema;
|
|
9150
9259
|
var SandboxError = errors_exports.SandboxError;
|
|
9151
9260
|
var optional = schema_exports$9.optional;
|
|
9152
|
-
export { control_exports as Control, db_exports as Database, DateTimeUtcFromMillis, event_exports as Event, list_exports as EventList, registry_exports as EventRegistry, EventSchema, global_exports as Global, harness_exports as Harness, location_exports as Location, plugin_exports as Plugin, schema_exports as PromptSchema, run_exports as Runner, sandbox_exports as Sandbox, SandboxError, session_exports as Session, session_exports$1 as SessionStore, settings_exports as Settings, state_exports as State, tool_exports as Tool, optional };
|
|
9261
|
+
export { control_exports as Control, db_exports as Database, DateTimeUtcFromMillis, event_exports as Event, list_exports as EventList, registry_exports as EventRegistry, EventSchema, global_exports as Global, harness_exports as Harness, location_exports as Location, catalog_exports as ModelCatalog, plugin_exports as Plugin, schema_exports as PromptSchema, run_exports as Runner, sandbox_exports as Sandbox, SandboxError, session_exports as Session, session_exports$1 as SessionStore, settings_exports as Settings, state_exports as State, tool_exports as Tool, optional };
|
|
9153
9262
|
|
|
9154
9263
|
//# sourceMappingURL=effect.mjs.map
|