@codeworksh/cli 0.3.0 → 0.4.0-canary.52.1.ge7df22a4
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/{acp-XRYX2dyw.mjs → acp-BPiMP9SV.mjs} +3 -3
- package/{acp-XRYX2dyw.mjs.map → acp-BPiMP9SV.mjs.map} +1 -1
- package/{add-Cih66f0Z.mjs → add-Dzsv-fKd.mjs} +3 -3
- package/{add-Cih66f0Z.mjs.map → add-Dzsv-fKd.mjs.map} +1 -1
- package/{check-CWXDjuC1.mjs → check-DhojkK_E.mjs} +3 -3
- package/{check-CWXDjuC1.mjs.map → check-DhojkK_E.mjs.map} +1 -1
- package/{entries-8LFGwy4n.mjs → entries-KpYvZdpi.mjs} +3 -3
- package/{entries-8LFGwy4n.mjs.map → entries-KpYvZdpi.mjs.map} +1 -1
- package/{error-CzUxh0Q9.mjs → error-BMI81XvR.mjs} +226 -7
- package/error-BMI81XvR.mjs.map +1 -0
- package/{identity-CSj_73kP.mjs → identity-Dl0hQHgK.mjs} +2 -2
- package/{identity-CSj_73kP.mjs.map → identity-Dl0hQHgK.mjs.map} +1 -1
- package/index.mjs +24 -24
- package/{install-CRGaL_bc.mjs → install-BLRDBzp1.mjs} +3 -3
- package/{install-CRGaL_bc.mjs.map → install-BLRDBzp1.mjs.map} +1 -1
- package/{link-BiwLQTXW.mjs → link--UnM2qnT.mjs} +2 -2
- package/{link-BiwLQTXW.mjs.map → link--UnM2qnT.mjs.map} +1 -1
- package/{list-BvBVZtHt.mjs → list-9fVqelid.mjs} +2 -2
- package/{list-BvBVZtHt.mjs.map → list-9fVqelid.mjs.map} +1 -1
- package/{list-Daq_zh4E.mjs → list-BE4xSEe3.mjs} +3 -3
- package/{list-Daq_zh4E.mjs.map → list-BE4xSEe3.mjs.map} +1 -1
- package/{login-lsagcqp6.mjs → login-CRwxi5WD.mjs} +3 -3
- package/{login-lsagcqp6.mjs.map → login-CRwxi5WD.mjs.map} +1 -1
- package/{logout-DdkYEWM6.mjs → logout-C89X0B9M.mjs} +3 -3
- package/{logout-DdkYEWM6.mjs.map → logout-C89X0B9M.mjs.map} +1 -1
- package/package-Biq8oAF2.mjs +6 -0
- package/package-Biq8oAF2.mjs.map +1 -0
- package/package.json +7 -6
- package/{providers-CZmPKpb_.mjs → providers-BrXJvY2P.mjs} +2 -2
- package/{providers-CZmPKpb_.mjs.map → providers-BrXJvY2P.mjs.map} +1 -1
- package/{refresh-BFxZjrH1.mjs → refresh--j0Pj4Cw.mjs} +2 -2
- package/{refresh-BFxZjrH1.mjs.map → refresh--j0Pj4Cw.mjs.map} +1 -1
- package/{refresh-Cbq-iY3f.mjs → refresh-D0wBP2Go.mjs} +3 -3
- package/{refresh-Cbq-iY3f.mjs.map → refresh-D0wBP2Go.mjs.map} +1 -1
- package/{reload-C6c48hBY.mjs → reload-DgPawyzr.mjs} +2 -2
- package/{reload-C6c48hBY.mjs.map → reload-DgPawyzr.mjs.map} +1 -1
- package/{remove-lig66J-D.mjs → remove-DfFme5w2.mjs} +3 -3
- package/{remove-lig66J-D.mjs.map → remove-DfFme5w2.mjs.map} +1 -1
- package/{restart-C0FtXDtK.mjs → restart-D3QLTXxy.mjs} +3 -3
- package/{restart-C0FtXDtK.mjs.map → restart-D3QLTXxy.mjs.map} +1 -1
- package/{run-DIFNYGN-.mjs → run-CVRkm_Iz.mjs} +3 -3
- package/{run-DIFNYGN-.mjs.map → run-CVRkm_Iz.mjs.map} +1 -1
- package/{serve-jhJP26fG.mjs → serve-D2BTAZpR.mjs} +108 -8
- package/serve-D2BTAZpR.mjs.map +1 -0
- package/{service-BP8Y1n1R.mjs → service-BI_FL26Q.mjs} +3 -3
- package/{service-BP8Y1n1R.mjs.map → service-BI_FL26Q.mjs.map} +1 -1
- package/{settings-BM_gw7Fm.mjs → settings-BfmoL27m.mjs} +2 -2
- package/{settings-BM_gw7Fm.mjs.map → settings-BfmoL27m.mjs.map} +1 -1
- package/{shared-wNZoC017.mjs → shared-CnuCjvcG.mjs} +2 -2
- package/{shared-wNZoC017.mjs.map → shared-CnuCjvcG.mjs.map} +1 -1
- package/{start-BBGmLmro.mjs → start-Bkk-Y6yE.mjs} +3 -3
- package/{start-BBGmLmro.mjs.map → start-Bkk-Y6yE.mjs.map} +1 -1
- package/{status-BmpmqhUX.mjs → status-BhkBSKLd.mjs} +3 -3
- package/{status-BmpmqhUX.mjs.map → status-BhkBSKLd.mjs.map} +1 -1
- package/{status-CQHqU7_7.mjs → status-DVzFDoek.mjs} +3 -3
- package/{status-CQHqU7_7.mjs.map → status-DVzFDoek.mjs.map} +1 -1
- package/{stop-DHzqbCeB.mjs → stop-COTZBIML.mjs} +3 -3
- package/{stop-DHzqbCeB.mjs.map → stop-COTZBIML.mjs.map} +1 -1
- package/{tui-BGZPK8sC.mjs → tui-Bk0VBJIB.mjs} +4 -4
- package/{tui-BGZPK8sC.mjs.map → tui-Bk0VBJIB.mjs.map} +1 -1
- package/{update-CfrkcWoW.mjs → update-D0XPVfJu.mjs} +3 -3
- package/{update-CfrkcWoW.mjs.map → update-D0XPVfJu.mjs.map} +1 -1
- package/error-CzUxh0Q9.mjs.map +0 -1
- package/package-DyEUYkEq.mjs +0 -6
- package/package-DyEUYkEq.mjs.map +0 -1
- package/serve-jhJP26fG.mjs.map +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { i as Cmd, t as handler } from "./runtime-B43doHjV.mjs";
|
|
2
|
-
import { _ as path, b as
|
|
2
|
+
import { _ as path, b as Core, g as OAuthError, h as Api, r as renderError, u as writeError, v as decode, x as encode, y as Connected } from "./error-BMI81XvR.mjs";
|
|
3
3
|
import { t as harnessOptions } from "./harness-6d8TlAVk.mjs";
|
|
4
|
-
import { c as register, r as layer$4, s as log, t as Service$2 } from "./identity-
|
|
5
|
-
import { Cause, Context, Effect, FileSystem, Layer, Logger, Option, Queue, Stream } from "effect";
|
|
4
|
+
import { c as register, r as layer$4, s as log, t as Service$2 } from "./identity-Dl0hQHgK.mjs";
|
|
5
|
+
import { Cause, Context, DateTime, Effect, FileSystem, Layer, Logger, Option, Queue, Stream } from "effect";
|
|
6
|
+
import { Model } from "@codeworksh/aikit";
|
|
6
7
|
import { HttpRouter, HttpServer } from "effect/http";
|
|
7
|
-
import { Control, Event, EventRegistry, EventSchema, Global, Harness, PromptSchema, Sandbox, Session, SessionStore, State } from "@codeworksh/harness/effect";
|
|
8
|
+
import { Control, Event, EventRegistry, EventSchema, Global, Harness, ModelCatalog, Project, PromptSchema, Sandbox, Session, SessionStore, Settings, State } from "@codeworksh/harness/effect";
|
|
8
9
|
import { NodeHttpServer } from "@effect/platform-node";
|
|
9
10
|
import { RpcSerialization, RpcServer } from "effect/rpc";
|
|
10
11
|
import { summarizeGitHubCopilot, summarizeOpenAICodex } from "@codeworksh/aikit/oauth/summary";
|
|
@@ -133,7 +134,12 @@ const toSessionInfo = (info) => ({
|
|
|
133
134
|
title: info.title,
|
|
134
135
|
directory: info.directory,
|
|
135
136
|
...info.hostDir === void 0 ? {} : { hostDir: info.hostDir },
|
|
136
|
-
...info.sandbox === void 0 ? {} : { sandbox: toSandboxInfo(info.sandbox) }
|
|
137
|
+
...info.sandbox === void 0 ? {} : { sandbox: toSandboxInfo(info.sandbox) },
|
|
138
|
+
...info.project === void 0 ? {} : { project: info.project },
|
|
139
|
+
updatedAt: info.updatedAt,
|
|
140
|
+
...info.model === void 0 ? {} : { model: info.model },
|
|
141
|
+
...info.thinkingLevel === void 0 ? {} : { thinkingLevel: info.thinkingLevel },
|
|
142
|
+
...info.parked ? { parked: true } : {}
|
|
137
143
|
});
|
|
138
144
|
const runtimeInput = (runtime) => ({
|
|
139
145
|
...runtime?.model === void 0 ? {} : { model: runtime.model },
|
|
@@ -149,6 +155,8 @@ const exported = (name, self) => self.pipe(Effect.withSpan(name, {
|
|
|
149
155
|
root: true
|
|
150
156
|
}));
|
|
151
157
|
const releaseOnFailure = (selection) => selection?.created && selection.info !== void 0 ? Sandbox.stop(selection.info.id).pipe(Effect.catchCause(Effect.logError)) : Effect.void;
|
|
158
|
+
/** A `session.log` item for the wire: a stored event as its envelope, or the replay marker. */
|
|
159
|
+
const toLogItem = (registry) => (item) => Event.isSynced(item) ? Effect.succeed(item) : encode(item, registry).pipe(Effect.catchTag("UnknownEventTypeError", Effect.die));
|
|
152
160
|
const layer$1 = Api.toLayer(Effect.gen(function* () {
|
|
153
161
|
const control = yield* Control.Service;
|
|
154
162
|
const feed = yield* Service$1;
|
|
@@ -156,6 +164,8 @@ const layer$1 = Api.toLayer(Effect.gen(function* () {
|
|
|
156
164
|
const state = yield* State.Service;
|
|
157
165
|
const oauth = yield* Service;
|
|
158
166
|
const identity = yield* Service$2;
|
|
167
|
+
const events = yield* Event.Service;
|
|
168
|
+
const registry = yield* EventRegistry.Service;
|
|
159
169
|
return Api.of({
|
|
160
170
|
"server.info": () => Effect.succeed({
|
|
161
171
|
pid: identity.pid,
|
|
@@ -165,7 +175,7 @@ const layer$1 = Api.toLayer(Effect.gen(function* () {
|
|
|
165
175
|
"auth.status": ({ provider }) => oauth.status(provider),
|
|
166
176
|
"auth.refresh": ({ provider }) => oauth.refresh(provider),
|
|
167
177
|
"auth.logout": ({ provider }) => oauth.logout(provider),
|
|
168
|
-
"session.create": Effect.fnUntraced(function* ({ title, directory, hostDir, sandbox, runtime }) {
|
|
178
|
+
"session.create": Effect.fnUntraced(function* ({ title, directory, hostDir, sandbox, runtime, project }) {
|
|
169
179
|
const selection = sandbox === void 0 ? void 0 : yield* exported("Server.session.create", Sandbox.resolve(sandbox));
|
|
170
180
|
const selected = selection?.info;
|
|
171
181
|
const handle = yield* Session.create({
|
|
@@ -173,10 +183,52 @@ const layer$1 = Api.toLayer(Effect.gen(function* () {
|
|
|
173
183
|
...directory === void 0 ? {} : { directory },
|
|
174
184
|
...hostDir === void 0 ? {} : { hostDir },
|
|
175
185
|
...selected === void 0 ? {} : { sandbox: selected },
|
|
186
|
+
...project === void 0 ? {} : { project },
|
|
176
187
|
...runtimeInput(runtime)
|
|
177
188
|
}).pipe(Effect.onError(() => releaseOnFailure(selection)));
|
|
178
189
|
return toSessionInfo(yield* handle.info);
|
|
179
190
|
}),
|
|
191
|
+
"model.list": Effect.fnUntraced(function* ({ hostDir }) {
|
|
192
|
+
const filter = hostDir === void 0 ? {} : { hostDir };
|
|
193
|
+
const loaded = yield* (yield* Settings.Service).load(hostDir);
|
|
194
|
+
const { model } = loaded;
|
|
195
|
+
const providers = yield* ModelCatalog.available(filter);
|
|
196
|
+
const entry = (yield* ModelCatalog.effective(loaded.models))[model.provider]?.[model.id];
|
|
197
|
+
return {
|
|
198
|
+
providers: providers.flatMap((provider) => {
|
|
199
|
+
const models = Object.values(provider.models);
|
|
200
|
+
const first = models[0];
|
|
201
|
+
return first === void 0 ? [] : [{
|
|
202
|
+
id: provider.id,
|
|
203
|
+
name: first.provider.name,
|
|
204
|
+
models: models.map((info) => ({
|
|
205
|
+
id: info.id,
|
|
206
|
+
name: info.name,
|
|
207
|
+
thinkingLevels: Model.getSupportedThinkingLevels(info)
|
|
208
|
+
}))
|
|
209
|
+
}];
|
|
210
|
+
}),
|
|
211
|
+
default: {
|
|
212
|
+
provider: model.provider,
|
|
213
|
+
id: model.id,
|
|
214
|
+
thinkingLevel: model.thinkingLevel,
|
|
215
|
+
...entry === void 0 ? {} : {
|
|
216
|
+
name: entry.name,
|
|
217
|
+
providerName: entry.provider.name
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}),
|
|
222
|
+
"project.rename": Effect.fnUntraced(function* ({ projectId, name }) {
|
|
223
|
+
const renamed = yield* Project.rename({
|
|
224
|
+
projectId,
|
|
225
|
+
name
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
id: renamed.id,
|
|
229
|
+
name: renamed.name
|
|
230
|
+
};
|
|
231
|
+
}),
|
|
180
232
|
"plugin.reload": Effect.fnUntraced(function* () {
|
|
181
233
|
return yield* state.reload;
|
|
182
234
|
}),
|
|
@@ -194,6 +246,35 @@ const layer$1 = Api.toLayer(Effect.gen(function* () {
|
|
|
194
246
|
"session.info": Effect.fnUntraced(function* ({ sessionId }) {
|
|
195
247
|
return yield* sessionInfo(sessionId);
|
|
196
248
|
}),
|
|
249
|
+
"session.history": Effect.fnUntraced(function* ({ sessionId }) {
|
|
250
|
+
if (Option.isNone(yield* sessions.get(sessionId))) return yield* new SessionStore.SessionNotFoundError({ sessionId });
|
|
251
|
+
return { entries: (yield* sessions.path(sessionId)).flatMap(({ entry, parts }) => entry.type === "user" || entry.type === "assistant" ? [{
|
|
252
|
+
id: entry.id,
|
|
253
|
+
seq: entry.seq,
|
|
254
|
+
role: entry.type,
|
|
255
|
+
state: entry.state,
|
|
256
|
+
createdAt: DateTime.toEpochMillis(entry.createdAt),
|
|
257
|
+
updatedAt: DateTime.toEpochMillis(entry.updatedAt),
|
|
258
|
+
parts: parts.flatMap((part) => Option.toArray(decode(part.data)))
|
|
259
|
+
}] : []) };
|
|
260
|
+
}),
|
|
261
|
+
"session.queue": Effect.fnUntraced(function* ({ sessionId }) {
|
|
262
|
+
if (Option.isNone(yield* sessions.get(sessionId))) return yield* new SessionStore.SessionNotFoundError({ sessionId });
|
|
263
|
+
return { entries: (yield* control.pending(sessionId)).map((input) => ({
|
|
264
|
+
id: input.id,
|
|
265
|
+
delivery: input.delivery,
|
|
266
|
+
text: PromptSchema.Prompt.text(input.prompt),
|
|
267
|
+
createdAt: DateTime.toEpochMillis(input.timeCreated)
|
|
268
|
+
})) };
|
|
269
|
+
}),
|
|
270
|
+
"session.log": ({ sessionId, after, follow }) => Stream.unwrap(Effect.gen(function* () {
|
|
271
|
+
if (Option.isNone(yield* sessions.get(sessionId))) return yield* new SessionStore.SessionNotFoundError({ sessionId });
|
|
272
|
+
return events.log({
|
|
273
|
+
aggregateId: sessionId,
|
|
274
|
+
follow: follow ?? false,
|
|
275
|
+
...after === void 0 ? {} : { after }
|
|
276
|
+
}).pipe(Stream.mapEffect(toLogItem(registry.get)));
|
|
277
|
+
})),
|
|
197
278
|
"session.link": Effect.fnUntraced(function* ({ sessionId, hostDir }) {
|
|
198
279
|
const handle = yield* Session.link({
|
|
199
280
|
sessionId,
|
|
@@ -222,6 +303,17 @@ const layer$1 = Api.toLayer(Effect.gen(function* () {
|
|
|
222
303
|
"session.interrupt": Effect.fnUntraced(function* ({ sessionId }) {
|
|
223
304
|
return { interrupted: yield* control.interrupt(sessionId, { reason: "user" }) };
|
|
224
305
|
}),
|
|
306
|
+
"session.cancel": Effect.fnUntraced(function* ({ sessionId, messageId }) {
|
|
307
|
+
if (Option.isNone(yield* sessions.get(sessionId))) return yield* new SessionStore.SessionNotFoundError({ sessionId });
|
|
308
|
+
return { cancelled: yield* control.cancel(sessionId, messageId) };
|
|
309
|
+
}),
|
|
310
|
+
"session.update": Effect.fnUntraced(function* ({ sessionId, messageId, delivery, text }) {
|
|
311
|
+
if (Option.isNone(yield* sessions.get(sessionId))) return yield* new SessionStore.SessionNotFoundError({ sessionId });
|
|
312
|
+
return { updated: yield* control.update(sessionId, messageId, {
|
|
313
|
+
...delivery === void 0 ? {} : { delivery },
|
|
314
|
+
...text === void 0 ? {} : { prompt: PromptSchema.Prompt.fromText(text) }
|
|
315
|
+
}) };
|
|
316
|
+
}),
|
|
225
317
|
"session.message": Effect.fnUntraced(function* ({ sessionId, messageId }) {
|
|
226
318
|
const found = yield* sessions.entry(messageId);
|
|
227
319
|
return { found: Option.isSome(found) && found.value.entry.sessionId === sessionId };
|
|
@@ -278,6 +370,14 @@ const managedShutdown = Layer.unwrap(Effect.gen(function* () {
|
|
|
278
370
|
return Layer.empty;
|
|
279
371
|
}));
|
|
280
372
|
/**
|
|
373
|
+
* Resumes the runs the last server left unfinished, whether it crashed or shut down: the session
|
|
374
|
+
* table records them as they start, so this needs nothing from the previous process.
|
|
375
|
+
*/
|
|
376
|
+
const resumeUnfinished = Layer.effectDiscard(Effect.gen(function* () {
|
|
377
|
+
const control = yield* Control.Service;
|
|
378
|
+
yield* Effect.forkScoped(control.resumeUnfinished);
|
|
379
|
+
}));
|
|
380
|
+
/**
|
|
281
381
|
* Advertises the listener once it has an address: logs the endpoint and files
|
|
282
382
|
* the registration later invocations discover it through.
|
|
283
383
|
*/
|
|
@@ -305,7 +405,7 @@ const layer = (options) => {
|
|
|
305
405
|
path,
|
|
306
406
|
protocol: "websocket"
|
|
307
407
|
}).pipe(Layer.provide(layer$1.pipe(Layer.provide(layer$3))));
|
|
308
|
-
return HttpRouter.serve(rpc, { disableListenLog: true }).pipe(Layer.provideMerge(managedShutdown), Layer.provide(layer$2(options.harness.home === void 0 ? {} : { home: options.harness.home }))).pipe(Layer.provideMerge(announce), Layer.provideMerge(NodeHttpServer.layer(createServer, {
|
|
408
|
+
return HttpRouter.serve(rpc, { disableListenLog: true }).pipe(Layer.provideMerge(managedShutdown), Layer.provideMerge(resumeUnfinished), Layer.provide(layer$2(options.harness.home === void 0 ? {} : { home: options.harness.home }))).pipe(Layer.provideMerge(announce), Layer.provideMerge(NodeHttpServer.layer(createServer, {
|
|
309
409
|
host: options.host,
|
|
310
410
|
port: options.port
|
|
311
411
|
})), Layer.provide(RpcSerialization.layerNdjson), Layer.provide(layer$4), Layer.provide(Harness.layer(options.harness)));
|
|
@@ -338,4 +438,4 @@ var serve_default = handler(Cmd.commands.serve, Effect.fn("CLI.serve", { level:
|
|
|
338
438
|
//#endregion
|
|
339
439
|
export { serve_default as default };
|
|
340
440
|
|
|
341
|
-
//# sourceMappingURL=serve-
|
|
441
|
+
//# sourceMappingURL=serve-D2BTAZpR.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve-D2BTAZpR.mjs","names":["Service","Envelope.Core","Envelope.encode","layer","layer","Contract.OAuthError","Envelope.encode","layer","EventFeed.Service","OAuth.Service","Identity.Service","MessagePart.decode","Identity.Service","Contract.path","Registration.register","Contract.Api","EventFeed.layer","OAuth.layer","Identity.layer","Runtime.handler","Server.layer","Registration.log"],"sources":["../../src/server/feed.ts","../../src/server/oauth.ts","../../src/server/handlers.ts","../../src/server/server.ts","../../src/cli/cmd/handlers/serve.ts"],"sourcesContent":["import { Event, EventRegistry, EventSchema } from \"@codeworksh/harness/effect\";\nimport { Cause, Context, Effect, Layer, Queue, Scope, Stream } from \"effect\";\nimport { EncodingError, type EventEnvelope, Envelope } from \"./envelope.ts\";\n\nexport const SubscriberCapacity = 4096;\n\nexport type Error = Event.SubscriptionOverflowError | EncodingError;\n\nexport interface Interface {\n\t/** A live envelope stream for one client; releasing the scope unregisters it. */\n\treadonly subscribe: Effect.Effect<Stream.Stream<EventEnvelope, Error>, never, Scope.Scope>;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@codeworksh/cli/server/feed/Service\") {}\n\n/**\n * Fan-out for `event.subscribe`. One listener on the event bus encodes each\n * event once and hands the same envelope to every client, rather than every\n * client re-encoding the same payload.\n *\n * Failure is per-cause, not per-feed: a client that cannot keep up fails alone\n * with its own overflow, while an event that cannot be encoded is a server bug\n * that disconnects everyone currently attached -- the feed stays usable for\n * whoever connects next. Types outside the server's registry are dropped\n * before they can consume a client's capacity.\n */\nexport const make = Effect.fn(\"EventFeed.make\")(function* (\n\tlisten: (subscriber: Event.Subscriber) => Effect.Effect<Event.Unsubscribe>,\n\toptions?: { readonly capacity?: number; readonly registry?: Envelope.Registry },\n) {\n\tconst capacity = options?.capacity ?? SubscriberCapacity;\n\t// Whatever this server knows how to describe, plugin registrations included.\n\tconst registry = options?.registry ?? Envelope.Core;\n\tconst subscribers = new Set<Queue.Queue<EventEnvelope, Error>>();\n\n\tconst failAll = (error: Error) =>\n\t\tEffect.sync(() => {\n\t\t\tconst current = Array.from(subscribers);\n\t\t\tsubscribers.clear();\n\t\t\tfor (const subscriber of current) Queue.failCauseUnsafe(subscriber, Cause.fail(error));\n\t\t});\n\n\tconst deliver = Effect.fnUntraced(function* (event: EventSchema.Payload) {\n\t\tif (subscribers.size === 0) return;\n\t\t// Dropped here rather than after the queue: a burst of events this server\n\t\t// cannot describe must not spend a client's capacity on its way to nowhere.\n\t\tif (registry(event.type) === undefined) return;\n\t\tconst envelope = yield* Envelope.encode(event, registry);\n\t\tfor (const subscriber of subscribers) {\n\t\t\tif (Queue.offerUnsafe(subscriber, envelope)) continue;\n\t\t\tsubscribers.delete(subscriber);\n\t\t\tQueue.failCauseUnsafe(subscriber, Cause.fail(new Event.SubscriptionOverflowError({ capacity })));\n\t\t}\n\t});\n\n\tconst publish = (event: EventSchema.Payload) =>\n\t\tdeliver(event).pipe(\n\t\t\tEffect.catchTags({\n\t\t\t\tEventEncodingError: (error) =>\n\t\t\t\t\tEffect.logError(\"Failed to encode public event\", { type: error.type, cause: error.cause }).pipe(\n\t\t\t\t\t\tEffect.andThen(failAll(error)),\n\t\t\t\t\t),\n\t\t\t\t// The registry guard in `deliver` already returned for unknown types.\n\t\t\t\tUnknownEventTypeError: Effect.die,\n\t\t\t}),\n\t\t);\n\n\tconst unsubscribe = yield* listen(publish);\n\tyield* Effect.addFinalizer(() => unsubscribe);\n\n\treturn Service.of({\n\t\tsubscribe: Effect.acquireRelease(\n\t\t\tQueue.dropping<EventEnvelope, Error>(capacity).pipe(\n\t\t\t\tEffect.tap((queue) => Effect.sync(() => subscribers.add(queue))),\n\t\t\t),\n\t\t\t(queue) => Effect.sync(() => subscribers.delete(queue)).pipe(Effect.andThen(Queue.shutdown(queue))),\n\t\t).pipe(Effect.map(Stream.fromQueue)),\n\t});\n});\n\nexport const layer = Layer.effect(\n\tService,\n\tEffect.gen(function* () {\n\t\tconst events = yield* Event.Service;\n\t\tconst registered = yield* EventRegistry.Service;\n\t\treturn yield* make(events.listen, { registry: registered.get });\n\t}),\n);\n\nexport * as EventFeed from \"./feed.ts\";\n","/**\n * Server-side OAuth credential storage.\n *\n * The CLI runs the interactive part of every login locally — a browser\n * callback or a device code cannot travel over RPC — and sends the resulting\n * credentials here, so this service only ever reads, writes, refreshes and\n * clears the server home's `auth.json`.\n */\nimport {\n\tGitHubCopilotOAuthClient,\n\tJsonGitHubCopilotAuthStorage,\n\ttype GitHubCopilotOAuthCredentials,\n} from \"@codeworksh/aikit/oauth/github/copilot\";\nimport {\n\tJsonOpenAICodexAuthStorage,\n\tOpenAICodexOAuthClient,\n\ttype OpenAICodexOAuthCredentials,\n} from \"@codeworksh/aikit/oauth/openai/codex\";\nimport { summarizeGitHubCopilot, summarizeOpenAICodex } from \"@codeworksh/aikit/oauth/summary\";\nimport { Global } from \"@codeworksh/harness/effect\";\nimport { Context, Effect, Layer } from \"effect\";\nimport { Contract } from \"./contract.ts\";\n\ninterface Interface {\n\treadonly save: (credentials: Contract.OAuthCredentials) => Effect.Effect<Contract.OAuthInfo, Contract.OAuthError>;\n\treadonly status: (provider: Contract.OAuthProvider) => Effect.Effect<Contract.OAuthInfo | null, Contract.OAuthError>;\n\treadonly refresh: (\n\t\tprovider: Contract.OAuthProvider,\n\t) => Effect.Effect<Contract.OAuthInfo | null, Contract.OAuthError>;\n\treadonly logout: (provider: Contract.OAuthProvider) => Effect.Effect<void, Contract.OAuthError>;\n}\n\nexport class Service extends Context.Service<Service, Interface>()(\"@codeworksh/cli/server/oauth/Service\") {}\n\nconst message = (cause: unknown): string =>\n\tcause instanceof Error && cause.message.trim().length > 0 ? cause.message : String(cause);\n\n/**\n * Re-exported so the server, the CLI and aikit all describe a login the same\n * way; `Contract.OAuthInfo` is the wire shape of {@link OAuthSummary}.\n */\nexport const codexInfo = summarizeOpenAICodex;\nexport const copilotInfo = summarizeGitHubCopilot;\n\nexport const layer = (options: { readonly home?: string | undefined } = {}) =>\n\tLayer.effect(\n\t\tService,\n\t\tEffect.gen(function* () {\n\t\t\tconst global = yield* Global.Service;\n\t\t\tconst authFile = Global.authFile(options.home === undefined ? undefined : global.home);\n\t\t\tconst file = authFile === undefined ? {} : { path: authFile };\n\t\t\tconst codex = new OpenAICodexOAuthClient({ storage: new JsonOpenAICodexAuthStorage(file) });\n\t\t\tconst copilot = new GitHubCopilotOAuthClient({ storage: new JsonGitHubCopilotAuthStorage(file) });\n\t\t\tconst attempt = <A>(run: () => Promise<A>) =>\n\t\t\t\tEffect.tryPromise({\n\t\t\t\t\ttry: run,\n\t\t\t\t\tcatch: (cause) => new Contract.OAuthError({ message: message(cause) }),\n\t\t\t\t});\n\t\t\tconst optional = <A>(run: () => Promise<A | undefined>, info: (value: A) => Contract.OAuthInfo) =>\n\t\t\t\tattempt(run).pipe(Effect.map((value) => (value === undefined ? null : info(value))));\n\n\t\t\treturn Service.of({\n\t\t\t\t// The `provider` tag is routing, not a credential: it never reaches disk.\n\t\t\t\tsave: (wire) => {\n\t\t\t\t\tif (wire.provider === \"openai-codex\") {\n\t\t\t\t\t\tconst credentials: OpenAICodexOAuthCredentials = {\n\t\t\t\t\t\t\taccess: wire.access,\n\t\t\t\t\t\t\trefresh: wire.refresh,\n\t\t\t\t\t\t\texpires: wire.expires,\n\t\t\t\t\t\t\taccountId: wire.accountId,\n\t\t\t\t\t\t};\n\t\t\t\t\t\treturn attempt(() => codex.storage.set(credentials)).pipe(Effect.as(codexInfo(credentials)));\n\t\t\t\t\t}\n\t\t\t\t\tconst credentials: GitHubCopilotOAuthCredentials = {\n\t\t\t\t\t\taccess: wire.access,\n\t\t\t\t\t\trefresh: wire.refresh,\n\t\t\t\t\t\texpires: wire.expires,\n\t\t\t\t\t\t...(wire.enterpriseUrl === undefined ? {} : { enterpriseUrl: wire.enterpriseUrl }),\n\t\t\t\t\t\t...(wire.apiEndpoint === undefined ? {} : { apiEndpoint: wire.apiEndpoint }),\n\t\t\t\t\t\t...(wire.availableModelIds === undefined ? {} : { availableModelIds: [...wire.availableModelIds] }),\n\t\t\t\t\t};\n\t\t\t\t\treturn attempt(() => copilot.storage.set(credentials)).pipe(Effect.as(copilotInfo(credentials)));\n\t\t\t\t},\n\t\t\t\tstatus: (provider) =>\n\t\t\t\t\tprovider === \"openai-codex\"\n\t\t\t\t\t\t? optional(() => codex.storage.get(), codexInfo)\n\t\t\t\t\t\t: optional(() => copilot.storage.get(), copilotInfo),\n\t\t\t\t// GitHub OAuth tokens never expire, so a Copilot \"refresh\" is a read.\n\t\t\t\trefresh: (provider) =>\n\t\t\t\t\tprovider === \"openai-codex\"\n\t\t\t\t\t\t? optional(() => codex.getCredentials(), codexInfo)\n\t\t\t\t\t\t: optional(() => copilot.getCredentials(), copilotInfo),\n\t\t\t\tlogout: (provider) => attempt(() => (provider === \"openai-codex\" ? codex.logout() : copilot.logout())),\n\t\t\t});\n\t\t}),\n\t);\n\nexport * as OAuth from \"./oauth.ts\";\n","import { Model } from \"@codeworksh/aikit\";\nimport {\n\tControl,\n\tEvent,\n\tEventRegistry,\n\tEventSchema,\n\tModelCatalog,\n\tProject,\n\tPromptSchema,\n\tSandbox,\n\tSession,\n\tSessionStore,\n\tSettings,\n\tState,\n} from \"@codeworksh/harness/effect\";\nimport { DateTime, Effect, Option, Stream } from \"effect\";\nimport {\n\tContract,\n\ttype HistoryEntry,\n\ttype LogSynced,\n\ttype RuntimeConfig,\n\ttype SandboxInfo,\n\ttype SessionInfo,\n} from \"./contract.ts\";\nimport { type EncodingError, Envelope, type EventEnvelope } from \"./envelope.ts\";\nimport { EventFeed } from \"./feed.ts\";\nimport { Identity } from \"./identity.ts\";\nimport { OAuth } from \"./oauth.ts\";\nimport { MessagePart } from \"./part.ts\";\n\nconst toSandboxInfo = (info: Sandbox.Info): SandboxInfo => ({\n\tid: info.id,\n\tdriver: info.driver,\n\tkind: info.kind,\n\tproviderResourceId: info.providerResourceId,\n\townership: info.ownership,\n\tstatus: info.status,\n\tusage: info.usage,\n\trefCount: info.refCount,\n\tcreatedAt: info.createdAt,\n\tupdatedAt: info.updatedAt,\n});\n\nconst toSessionInfo = (info: Session.Info): SessionInfo => ({\n\tid: info.id,\n\ttitle: info.title,\n\tdirectory: info.directory,\n\t...(info.hostDir === undefined ? {} : { hostDir: info.hostDir }),\n\t...(info.sandbox === undefined ? {} : { sandbox: toSandboxInfo(info.sandbox) }),\n\t...(info.project === undefined ? {} : { project: info.project }),\n\tupdatedAt: info.updatedAt,\n\t...(info.model === undefined ? {} : { model: info.model }),\n\t...(info.thinkingLevel === undefined ? {} : { thinkingLevel: info.thinkingLevel }),\n\t...(info.parked ? { parked: true } : {}),\n});\n\n// Wire configs name only the keys they set, so `undefined` never reaches the\n// harness inputs under exactOptionalPropertyTypes.\nconst runtimeInput = (runtime: RuntimeConfig | undefined): Session.RuntimeInput => ({\n\t...(runtime?.model === undefined ? {} : { model: runtime.model }),\n\t...(runtime?.thinkingLevel === undefined ? {} : { thinkingLevel: runtime.thinkingLevel }),\n});\n\nconst sessionInfo = Effect.fn(\"Server.sessionInfo\")(function* (sessionId: Session.SessionSchema.ID) {\n\tconst found = yield* Session.get(sessionId);\n\tif (Option.isNone(found)) return yield* new SessionStore.SessionNotFoundError({ sessionId });\n\treturn toSessionInfo(yield* found.value.info);\n});\n\n// RpcServer's request span has no level, so it is Debug and drops every Info span under it.\n// Provisioning a sandbox is worth exporting, so it starts a trace of its own.\nconst exported = <A, E, R>(name: string, self: Effect.Effect<A, E, R>) =>\n\tself.pipe(Effect.withSpan(name, { level: \"Info\", root: true }));\n\nconst releaseOnFailure = (selection: Effect.Success<ReturnType<typeof Sandbox.resolve>> | undefined) =>\n\tselection?.created && selection.info !== undefined\n\t\t? Sandbox.stop(selection.info.id).pipe(Effect.catchCause(Effect.logError))\n\t\t: Effect.void;\n\n/** A `session.log` item for the wire: a stored event as its envelope, or the replay marker. */\nconst toLogItem =\n\t(registry: Envelope.Registry) =>\n\t(item: Event.LogItem): Effect.Effect<EventEnvelope | LogSynced, EncodingError> =>\n\t\tEvent.isSynced(item)\n\t\t\t? Effect.succeed(item)\n\t\t\t: Envelope.encode(item, registry).pipe(\n\t\t\t\t\t// Every stored type is a kernel durable one, which the registry always knows.\n\t\t\t\t\tEffect.catchTag(\"UnknownEventTypeError\", Effect.die),\n\t\t\t\t);\n\nexport const layer = Contract.Api.toLayer(\n\tEffect.gen(function* () {\n\t\tconst control = yield* Control.Service;\n\t\tconst feed = yield* EventFeed.Service;\n\t\tconst sessions = yield* SessionStore.Service;\n\t\tconst state = yield* State.Service;\n\t\tconst oauth = yield* OAuth.Service;\n\t\tconst identity = yield* Identity.Service;\n\t\tconst events = yield* Event.Service;\n\t\tconst registry = yield* EventRegistry.Service;\n\n\t\treturn Contract.Api.of({\n\t\t\t\"server.info\": () => Effect.succeed({ pid: identity.pid, version: identity.version }),\n\t\t\t\"auth.save\": ({ credentials }) => oauth.save(credentials),\n\t\t\t\"auth.status\": ({ provider }) => oauth.status(provider),\n\t\t\t\"auth.refresh\": ({ provider }) => oauth.refresh(provider),\n\t\t\t\"auth.logout\": ({ provider }) => oauth.logout(provider),\n\t\t\t\"session.create\": Effect.fnUntraced(function* ({ title, directory, hostDir, sandbox, runtime, project }) {\n\t\t\t\tconst selection =\n\t\t\t\t\tsandbox === undefined ? undefined : yield* exported(\"Server.session.create\", Sandbox.resolve(sandbox));\n\t\t\t\tconst selected = selection?.info;\n\t\t\t\tconst handle = yield* Session.create({\n\t\t\t\t\t...(title === undefined ? {} : { title }),\n\t\t\t\t\t...(directory === undefined ? {} : { directory }),\n\t\t\t\t\t// Passed through as given, never defaulted to the server's own directory: a\n\t\t\t\t\t// session the client did not place has no project, which is a normal state.\n\t\t\t\t\t...(hostDir === undefined ? {} : { hostDir }),\n\t\t\t\t\t...(selected === undefined ? {} : { sandbox: selected }),\n\t\t\t\t\t...(project === undefined ? {} : { project }),\n\t\t\t\t\t...runtimeInput(runtime),\n\t\t\t\t}).pipe(Effect.onError(() => releaseOnFailure(selection)));\n\t\t\t\treturn toSessionInfo(yield* handle.info);\n\t\t\t}),\n\t\t\t\"model.list\": Effect.fnUntraced(function* ({ hostDir }) {\n\t\t\t\tconst filter = hostDir === undefined ? {} : { hostDir };\n\t\t\t\tconst settings = yield* Settings.Service;\n\t\t\t\tconst loaded = yield* settings.load(hostDir);\n\t\t\t\tconst { model } = loaded;\n\t\t\t\tconst providers = yield* ModelCatalog.available(filter);\n\t\t\t\t// Named from the whole catalog, so a default without credentials still reads as itself.\n\t\t\t\tconst entry = (yield* ModelCatalog.effective(loaded.models))[model.provider]?.[model.id];\n\t\t\t\treturn {\n\t\t\t\t\tproviders: providers.flatMap((provider) => {\n\t\t\t\t\t\tconst models = Object.values(provider.models);\n\t\t\t\t\t\tconst first = models[0];\n\t\t\t\t\t\treturn first === undefined\n\t\t\t\t\t\t\t? []\n\t\t\t\t\t\t\t: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tid: provider.id,\n\t\t\t\t\t\t\t\t\t\tname: first.provider.name,\n\t\t\t\t\t\t\t\t\t\tmodels: models.map((info) => ({\n\t\t\t\t\t\t\t\t\t\t\tid: info.id,\n\t\t\t\t\t\t\t\t\t\t\tname: info.name,\n\t\t\t\t\t\t\t\t\t\t\tthinkingLevels: Model.getSupportedThinkingLevels(info),\n\t\t\t\t\t\t\t\t\t\t})),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\t}),\n\t\t\t\t\tdefault: {\n\t\t\t\t\t\tprovider: model.provider,\n\t\t\t\t\t\tid: model.id,\n\t\t\t\t\t\tthinkingLevel: model.thinkingLevel,\n\t\t\t\t\t\t...(entry === undefined ? {} : { name: entry.name, providerName: entry.provider.name }),\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}),\n\t\t\t\"project.rename\": Effect.fnUntraced(function* ({ projectId, name }) {\n\t\t\t\tconst renamed = yield* Project.rename({ projectId, name });\n\t\t\t\treturn { id: renamed.id, name: renamed.name };\n\t\t\t}),\n\t\t\t\"plugin.reload\": Effect.fnUntraced(function* () {\n\t\t\t\treturn yield* state.reload;\n\t\t\t}),\n\t\t\t\"session.list\": Effect.fnUntraced(function* () {\n\t\t\t\tconst rows = yield* sessions.list();\n\t\t\t\treturn yield* Effect.forEach(rows, (row) => sessionInfo(row.id).pipe(Effect.orDie));\n\t\t\t}),\n\t\t\t\"session.configure\": Effect.fnUntraced(function* ({ sessionId, runtime }) {\n\t\t\t\tconst handle = yield* Session.attach({ sessionId, ...runtimeInput(runtime) });\n\t\t\t\treturn toSessionInfo(yield* handle.info);\n\t\t\t}),\n\t\t\t\"session.info\": Effect.fnUntraced(function* ({ sessionId }) {\n\t\t\t\treturn yield* sessionInfo(sessionId);\n\t\t\t}),\n\t\t\t\"session.history\": Effect.fnUntraced(function* ({ sessionId }) {\n\t\t\t\tif (Option.isNone(yield* sessions.get(sessionId))) {\n\t\t\t\t\treturn yield* new SessionStore.SessionNotFoundError({ sessionId });\n\t\t\t\t}\n\t\t\t\tconst path = yield* sessions.path(sessionId);\n\t\t\t\tconst entries = path.flatMap(({ entry, parts }): ReadonlyArray<HistoryEntry> =>\n\t\t\t\t\tentry.type === \"user\" || entry.type === \"assistant\"\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tid: entry.id,\n\t\t\t\t\t\t\t\t\tseq: entry.seq,\n\t\t\t\t\t\t\t\t\trole: entry.type,\n\t\t\t\t\t\t\t\t\tstate: entry.state,\n\t\t\t\t\t\t\t\t\tcreatedAt: DateTime.toEpochMillis(entry.createdAt),\n\t\t\t\t\t\t\t\t\tupdatedAt: DateTime.toEpochMillis(entry.updatedAt),\n\t\t\t\t\t\t\t\t\t// A part this schema does not render (a future aikit type) is skipped, not fatal.\n\t\t\t\t\t\t\t\t\tparts: parts.flatMap((part) => Option.toArray(MessagePart.decode(part.data))),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: [],\n\t\t\t\t);\n\t\t\t\treturn { entries };\n\t\t\t}),\n\t\t\t\"session.queue\": Effect.fnUntraced(function* ({ sessionId }) {\n\t\t\t\tif (Option.isNone(yield* sessions.get(sessionId))) {\n\t\t\t\t\treturn yield* new SessionStore.SessionNotFoundError({ sessionId });\n\t\t\t\t}\n\t\t\t\tconst pending = yield* control.pending(sessionId);\n\t\t\t\treturn {\n\t\t\t\t\tentries: pending.map((input) => ({\n\t\t\t\t\t\tid: input.id,\n\t\t\t\t\t\tdelivery: input.delivery,\n\t\t\t\t\t\ttext: PromptSchema.Prompt.text(input.prompt),\n\t\t\t\t\t\tcreatedAt: DateTime.toEpochMillis(input.timeCreated),\n\t\t\t\t\t})),\n\t\t\t\t};\n\t\t\t}),\n\t\t\t\"session.log\": ({ sessionId, after, follow }) =>\n\t\t\t\tStream.unwrap(\n\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\tif (Option.isNone(yield* sessions.get(sessionId))) {\n\t\t\t\t\t\t\treturn yield* new SessionStore.SessionNotFoundError({ sessionId });\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn events\n\t\t\t\t\t\t\t.log({\n\t\t\t\t\t\t\t\taggregateId: sessionId,\n\t\t\t\t\t\t\t\tfollow: follow ?? false,\n\t\t\t\t\t\t\t\t...(after === undefined ? {} : { after }),\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.pipe(Stream.mapEffect(toLogItem(registry.get)));\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t\"session.link\": Effect.fnUntraced(function* ({ sessionId, hostDir }) {\n\t\t\t\tconst handle = yield* Session.link({ sessionId, hostDir: hostDir ?? null });\n\t\t\t\treturn toSessionInfo(yield* handle.info);\n\t\t\t}),\n\t\t\t\"session.relink\": Effect.fnUntraced(function* ({ sessionId, sandbox, directory }) {\n\t\t\t\tconst selection =\n\t\t\t\t\tsandbox === undefined ? undefined : yield* exported(\"Server.session.relink\", Sandbox.resolve(sandbox));\n\t\t\t\tconst selected = selection?.info;\n\t\t\t\tconst handle = yield* Session.relink({\n\t\t\t\t\tsessionId,\n\t\t\t\t\t...(selected === undefined ? {} : { sandbox: selected }),\n\t\t\t\t\t...(directory === undefined ? {} : { directory }),\n\t\t\t\t}).pipe(Effect.onError(() => releaseOnFailure(selection)));\n\t\t\t\treturn toSessionInfo(yield* handle.info);\n\t\t\t}),\n\t\t\t\"session.prompt\": Effect.fnUntraced(function* ({ sessionId, text, delivery, id }) {\n\t\t\t\tyield* control.prompt({\n\t\t\t\t\tsessionId,\n\t\t\t\t\tprompt: PromptSchema.Prompt.fromText(text),\n\t\t\t\t\t...(delivery === undefined ? {} : { delivery }),\n\t\t\t\t\t...(id === undefined ? {} : { id }),\n\t\t\t\t});\n\t\t\t}),\n\t\t\t\"session.interrupt\": Effect.fnUntraced(function* ({ sessionId }) {\n\t\t\t\t// Ack only: the wire answers \"was there work to stop\", and the caller\n\t\t\t\t// uses `session.wait` when it needs cleanup to have finished.\n\t\t\t\treturn { interrupted: yield* control.interrupt(sessionId, { reason: \"user\" }) };\n\t\t\t}),\n\t\t\t\"session.cancel\": Effect.fnUntraced(function* ({ sessionId, messageId }) {\n\t\t\t\tif (Option.isNone(yield* sessions.get(sessionId))) {\n\t\t\t\t\treturn yield* new SessionStore.SessionNotFoundError({ sessionId });\n\t\t\t\t}\n\t\t\t\treturn { cancelled: yield* control.cancel(sessionId, messageId) };\n\t\t\t}),\n\t\t\t\"session.update\": Effect.fnUntraced(function* ({ sessionId, messageId, delivery, text }) {\n\t\t\t\tif (Option.isNone(yield* sessions.get(sessionId))) {\n\t\t\t\t\treturn yield* new SessionStore.SessionNotFoundError({ sessionId });\n\t\t\t\t}\n\t\t\t\tconst updated = yield* control.update(sessionId, messageId, {\n\t\t\t\t\t...(delivery === undefined ? {} : { delivery }),\n\t\t\t\t\t...(text === undefined ? {} : { prompt: PromptSchema.Prompt.fromText(text) }),\n\t\t\t\t});\n\t\t\t\treturn { updated };\n\t\t\t}),\n\t\t\t\"session.message\": Effect.fnUntraced(function* ({ sessionId, messageId }) {\n\t\t\t\tconst found = yield* sessions.entry(messageId);\n\t\t\t\t// Scoped to the session the caller named: an entry id is global, and a\n\t\t\t\t// client should not learn about conversations it did not ask for.\n\t\t\t\treturn { found: Option.isSome(found) && found.value.entry.sessionId === sessionId };\n\t\t\t}),\n\t\t\t\"session.wait\": Effect.fnUntraced(function* ({ sessionId }) {\n\t\t\t\tyield* control.wait(sessionId);\n\t\t\t}),\n\t\t\t\"sandbox.drivers\": Effect.fnUntraced(function* () {\n\t\t\t\tconst drivers = yield* Sandbox.drivers();\n\t\t\t\treturn drivers.map(({ name, kind }) => ({ name, kind }));\n\t\t\t}),\n\t\t\t\"sandbox.list\": Effect.fnUntraced(function* () {\n\t\t\t\treturn (yield* Sandbox.list()).map(toSandboxInfo);\n\t\t\t}),\n\t\t\t\"sandbox.create\": Effect.fnUntraced(function* ({ driver }) {\n\t\t\t\treturn toSandboxInfo(yield* exported(\"Server.sandbox.create\", Sandbox.create({ driver })));\n\t\t\t}),\n\t\t\t\"sandbox.register\": Effect.fnUntraced(function* ({ driver, providerResourceId }) {\n\t\t\t\treturn toSandboxInfo(yield* Sandbox.register({ driver, providerResourceId }));\n\t\t\t}),\n\t\t\t\"sandbox.stop\": Effect.fnUntraced(function* ({ sandboxId }) {\n\t\t\t\tyield* Sandbox.stop(sandboxId);\n\t\t\t}),\n\t\t\t// Acquiring the feed subscription is the readiness barrier: `server.connected`\n\t\t\t// is only emitted once this client is registered, so a prompt admitted after\n\t\t\t// it cannot land in the gap.\n\t\t\t\"event.subscribe\": () =>\n\t\t\t\tStream.unwrap(\n\t\t\t\t\tfeed.subscribe.pipe(\n\t\t\t\t\t\tEffect.map((events) =>\n\t\t\t\t\t\t\tStream.make({ id: EventSchema.ID.create(), type: Envelope.Connected.type, data: {} }).pipe(\n\t\t\t\t\t\t\t\tStream.concat(events),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t});\n\t}),\n);\n\nexport * as Handlers from \"./handlers.ts\";\n","import { Control, Global, Harness, Sandbox } from \"@codeworksh/harness/effect\";\nimport { NodeHttpServer } from \"@effect/platform-node\";\nimport { Effect, Layer } from \"effect\";\nimport { HttpRouter, HttpServer } from \"effect/http\";\nimport * as NetAddress from \"effect/net/NetAddress\";\nimport { RpcSerialization, RpcServer } from \"effect/rpc\";\n/* @effect-diagnostics nodeBuiltinImport:off -- the RPC listener needs a raw node server. */\nimport { createServer } from \"node:http\";\nimport { Contract } from \"./contract.ts\";\nimport { EventFeed } from \"./feed.ts\";\nimport { Handlers } from \"./handlers.ts\";\nimport { Identity } from \"./identity.ts\";\nimport { OAuth } from \"./oauth.ts\";\nimport { Registration } from \"./registration.ts\";\n\n// Managed instances outlive any single RPC; the serve scope is what stops them.\nconst managedShutdown = Layer.unwrap(\n\tEffect.gen(function* () {\n\t\tconst control = yield* Control.Service;\n\t\tyield* Effect.addFinalizer(() =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\t// Release drain leases before stopping the server's managed sandboxes.\n\t\t\t\t// Both phases are best effort and bounded so a provider cannot prevent\n\t\t\t\t// process shutdown; an interrupted stop is persisted as faulted and can\n\t\t\t\t// recover the next time that sandbox is mounted.\n\t\t\t\tyield* Effect.forEach(\n\t\t\t\t\tyield* control.active,\n\t\t\t\t\t(id) =>\n\t\t\t\t\t\tcontrol\n\t\t\t\t\t\t\t.interrupt(id, { reason: \"shutdown\", awaitSettlement: true })\n\t\t\t\t\t\t\t.pipe(Effect.timeout(\"10 seconds\"), Effect.ignore),\n\t\t\t\t\t{ concurrency: \"unbounded\", discard: true },\n\t\t\t\t);\n\t\t\t\tconst infos = yield* Sandbox.list();\n\t\t\t\tyield* Effect.forEach(\n\t\t\t\t\tinfos.filter((info) => info.ownership === \"managed\"),\n\t\t\t\t\t(info) =>\n\t\t\t\t\t\tSandbox.stop(info.id).pipe(\n\t\t\t\t\t\t\tEffect.timeout(\"10 seconds\"),\n\t\t\t\t\t\t\tEffect.catchCause((cause) =>\n\t\t\t\t\t\t\t\tEffect.logError(\"Failed to stop managed sandbox during shutdown\", cause).pipe(\n\t\t\t\t\t\t\t\t\tEffect.annotateLogs({ sandboxInstanceId: info.id }),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t{ concurrency: \"unbounded\", discard: true },\n\t\t\t\t);\n\t\t\t}),\n\t\t);\n\t\treturn Layer.empty;\n\t}),\n);\n\n/**\n * Resumes the runs the last server left unfinished, whether it crashed or shut down: the session\n * table records them as they start, so this needs nothing from the previous process.\n */\nconst resumeUnfinished = Layer.effectDiscard(\n\tEffect.gen(function* () {\n\t\tconst control = yield* Control.Service;\n\t\tyield* Effect.forkScoped(control.resumeUnfinished);\n\t}),\n);\n\n/**\n * Advertises the listener once it has an address: logs the endpoint and files\n * the registration later invocations discover it through.\n */\nconst announce = Layer.unwrap(\n\tEffect.gen(function* () {\n\t\tconst server = yield* HttpServer.HttpServer;\n\t\tconst global = yield* Global.Service;\n\t\tconst identity = yield* Identity.Service;\n\t\t// A ws server always binds TCP; the guard keeps the logger honest if that ever changes.\n\t\tconst address = server.address;\n\t\tconst url = `${NetAddress.formatUrlUnsafe(address, \"ws\")}${Contract.path}`;\n\t\tyield* Effect.log(`codework serve listening on ${url}`);\n\t\tif (!NetAddress.isInetAddress(address)) return Layer.empty;\n\t\treturn Registration.register({\n\t\t\thome: global.home,\n\t\t\tinfo: { pid: identity.pid, version: identity.version, url, port: address.port },\n\t\t});\n\t}),\n);\n\nexport const layer = (options: { host: string; port: number; harness: Harness.Options }) => {\n\tconst rpc = RpcServer.layerHttp({ group: Contract.Api, path: Contract.path, protocol: \"websocket\" }).pipe(\n\t\tLayer.provide(Handlers.layer.pipe(Layer.provide(EventFeed.layer))),\n\t);\n\tconst app = HttpRouter.serve(rpc, { disableListenLog: true }).pipe(\n\t\tLayer.provideMerge(managedShutdown),\n\t\tLayer.provideMerge(resumeUnfinished),\n\t\tLayer.provide(OAuth.layer(options.harness.home === undefined ? {} : { home: options.harness.home })),\n\t);\n\treturn app.pipe(\n\t\tLayer.provideMerge(announce),\n\t\tLayer.provideMerge(NodeHttpServer.layer(createServer, { host: options.host, port: options.port })),\n\t\tLayer.provide(RpcSerialization.layerNdjson),\n\t\tLayer.provide(Identity.layer),\n\t\tLayer.provide(Harness.layer(options.harness)),\n\t);\n};\n\nexport * as Server from \"./server.ts\";\n","import { Global } from \"@codeworksh/harness/effect\";\nimport { Effect, FileSystem, Layer, Logger } from \"effect\";\nimport { Runtime } from \"../../../framework/runtime.ts\";\nimport { Registration } from \"../../../server/registration.ts\";\nimport { Server } from \"../../../server/server.ts\";\nimport { renderError } from \"../../error.ts\";\nimport { harnessOptions } from \"../../harness.ts\";\nimport { writeError } from \"../../output.ts\";\nimport { Cmd } from \"../cmd.ts\";\n\nconst failed = Effect.sync(() => {\n\tprocess.exitCode = 1;\n});\n\nconst report = (error: unknown) => writeError(renderError(error)).pipe(Effect.andThen(failed));\n\nexport default Runtime.handler(\n\tCmd.commands.serve,\n\tEffect.fn(\"CLI.serve\", { level: \"Info\" })(function* ({ host, port, service }) {\n\t\tconst shared = yield* Cmd.spec;\n\t\tconst harness = { ...harnessOptions(shared), watchModels: true };\n\t\tconst serve = Layer.launch(Server.layer({ host, port, harness }));\n\t\tif (!service) return yield* serve.pipe(Effect.catch(report));\n\n\t\t// A background server has no terminal: its log, and the reason it stopped, go to a\n\t\t// file the CLI that started it reads back when startup fails.\n\t\tconst background = Effect.gen(function* () {\n\t\t\tconst paths = yield* Global.resolve(harness.home === undefined ? {} : { home: harness.home });\n\t\t\tconst fs = yield* FileSystem.FileSystem;\n\t\t\tyield* fs.makeDirectory(paths.log, { recursive: true });\n\t\t\tconst logger = Logger.formatSimple.pipe(Logger.toFile(Registration.log(paths.log), { flag: \"w\" }));\n\t\t\tyield* serve.pipe(\n\t\t\t\tEffect.catch((error) => Effect.logError(renderError(error).trim()).pipe(Effect.andThen(failed))),\n\t\t\t\tEffect.provide(Logger.layer([logger])),\n\t\t\t);\n\t\t});\n\t\tyield* background.pipe(Effect.catch(report));\n\t}),\n);\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { f as layer, p as make } from "./error-
|
|
2
|
-
import { a as file, i as command, n as current, o as infoJson, s as log } from "./identity-
|
|
1
|
+
import { f as layer, p as make } from "./error-BMI81XvR.mjs";
|
|
2
|
+
import { a as file, i as command, n as current, o as infoJson, s as log } from "./identity-Dl0hQHgK.mjs";
|
|
3
3
|
import { Effect, FileSystem, Layer, Option, Ref, Schedule, Schema } from "effect";
|
|
4
4
|
import { Global } from "@codeworksh/harness/effect";
|
|
5
5
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
@@ -219,4 +219,4 @@ const options = (shared, server) => home(shared).pipe(Effect.map((paths) => ({
|
|
|
219
219
|
//#endregion
|
|
220
220
|
export { keepAlive as a, ensure as i, options as n, stop as o, discover as r, probe as s, home as t };
|
|
221
221
|
|
|
222
|
-
//# sourceMappingURL=service-
|
|
222
|
+
//# sourceMappingURL=service-BI_FL26Q.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-BP8Y1n1R.mjs","names":["Client.layer","Client.make","Registration.infoJson","Health.probe","Registration.file","log","Registration.log","Registration.command"],"sources":["../../src/server/health.ts","../../src/server/discovery.ts","../../src/cli/service.ts"],"sourcesContent":["import { Effect, Layer } from \"effect\";\nimport { Client } from \"./client.ts\";\n\n/**\n * One `server.info` call, with the answer normalized to `undefined` for every\n * way a server can fail to answer: unreachable, slow, or speaking a different\n * protocol. A health check that throws where it should say \"no\" would make\n * every caller wrap it in a catch.\n */\nexport const probe = (url: string) =>\n\tEffect.gen(function* () {\n\t\tconst context = yield* Layer.build(Client.layer(url)).pipe(Effect.orElseSucceed(() => undefined));\n\t\tif (context === undefined) return undefined;\n\t\treturn yield* Effect.gen(function* () {\n\t\t\tconst rpc = yield* Client.make;\n\t\t\treturn yield* rpc[\"server.info\"]({}).pipe(Effect.timeout(\"1 second\"));\n\t\t}).pipe(\n\t\t\tEffect.provideContext(context),\n\t\t\tEffect.orElseSucceed(() => undefined),\n\t\t);\n\t}).pipe(Effect.scoped);\n\nexport * as Health from \"./health.ts\";\n","import { type Duration, Effect, FileSystem, Option, Ref, Schedule, Schema } from \"effect\";\nimport { ChildProcess, ChildProcessSpawner } from \"effect/process\";\nimport { Health } from \"./health.ts\";\nimport { Identity } from \"./identity.ts\";\nimport { Registration } from \"./registration.ts\";\n\n/** A server a client can connect to, and the process and port behind it. */\nexport interface Endpoint {\n\treadonly url: string;\n\treadonly pid: number;\n\treadonly port: number;\n}\n\n/** A managed server could not be produced, in the words the CLI prints. */\nexport class ServerStartError extends Schema.TaggedError<ServerStartError>()(\"Discovery.ServerStartError\", {\n\tmessage: Schema.String,\n}) {}\n\nexport interface Options {\n\t/** The CodeWork home whose `service.json` names the server. */\n\treadonly home: string;\n\t/** The home's log directory, where a background server writes `server.log`. */\n\treadonly log: string;\n\treadonly host: string;\n\treadonly port: number;\n\t/** Server-owned flags placed before `serve`. */\n\treadonly args: ReadonlyArray<string>;\n}\n\nconst START_TIMEOUT = \"20 seconds\";\nconst RACE_GRACE = \"2 seconds\";\nconst STOP_TIMEOUT = \"5 seconds\";\n\nconst decode = Schema.decodeUnknownEffect(Registration.infoJson);\n\nconst read = (path: string) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tconst text = yield* fs.readFileString(path).pipe(Effect.option);\n\t\tif (Option.isNone(text)) return undefined;\n\t\treturn yield* decode(text.value).pipe(Effect.option, Effect.map(Option.getOrUndefined));\n\t});\n\nconst remove = (path: string) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tyield* fs.remove(path);\n\t}).pipe(Effect.ignore);\n\n/** Whether `pid` is a running process. EPERM means it exists but belongs to someone else. */\nconst alive = (pid: number) =>\n\tEffect.try({ try: () => process.kill(pid, 0), catch: (error) => (error as NodeJS.ErrnoException).code }).pipe(\n\t\tEffect.as(true),\n\t\tEffect.catch((code) => Effect.succeed(code === \"EPERM\")),\n\t);\n\n/** Whether `pid` exits within `timeout`. */\nconst exits = (pid: number, timeout: typeof STOP_TIMEOUT) =>\n\talive(pid).pipe(\n\t\tEffect.repeat({ while: (running) => running, schedule: Schedule.spaced(\"50 millis\") }),\n\t\tEffect.timeoutOption(timeout),\n\t\tEffect.map(Option.isSome),\n\t);\n\n/**\n * The registered server, with `live` set when the process answering its endpoint\n * is the one that registered. The file is a hint, never proof: a different server\n * on a reused port must not pass for it.\n */\nconst registered = (path: string) =>\n\tEffect.gen(function* () {\n\t\tconst info = yield* read(path);\n\t\tif (info === undefined) return undefined;\n\t\tconst live = yield* Health.probe(info.url);\n\t\treturn { info, live: live?.pid === info.pid ? live : undefined };\n\t});\n\nconst endpoint = (info: Registration.Info): Endpoint => ({ url: info.url, pid: info.pid, port: info.port });\n\n/** The running server registered for `home`, with the build it reports. */\nexport const discover = (home: string) =>\n\tregistered(Registration.file(home)).pipe(\n\t\tEffect.map((found) =>\n\t\t\tfound?.live === undefined ? undefined : { ...endpoint(found.info), version: found.live.version },\n\t\t),\n\t);\n\n/**\n * Stop the server registered for `home`, returning it, or undefined when none\n * answers. Only a server that confirms it is the registrant is signalled: a pid\n * on file may since have been reused by an unrelated process.\n */\nexport const stop = (home: string) =>\n\tEffect.gen(function* () {\n\t\tconst path = Registration.file(home);\n\t\tconst found = yield* registered(path);\n\t\tif (found === undefined) return undefined;\n\t\tif (found.live === undefined) {\n\t\t\tif (!(yield* alive(found.info.pid))) yield* remove(path);\n\t\t\treturn undefined;\n\t\t}\n\t\tyield* terminate(path, found.info.pid);\n\t\treturn endpoint(found.info);\n\t});\n\n/**\n * Ensure a healthy local server for `home`: reuse the registered one, replace it\n * when `replace` is set and it runs a different build, or start a background one.\n *\n * The background server is detached and outlives this caller, so the next\n * invocation reuses it. When two callers race, only the contender that binds the\n * port registers; the loser exits and both adopt the winner, because a caller\n * only ever believes a registration it can confirm over the wire.\n */\nexport const ensure = (\n\toptions: Options & {\n\t\treadonly replace: boolean;\n\t\treadonly onStart?: (reason: \"missing\" | \"version-mismatch\") => void;\n\t},\n) =>\n\tEffect.gen(function* () {\n\t\tconst path = Registration.file(options.home);\n\t\tconst found = yield* registered(path);\n\t\tif (found?.live !== undefined) {\n\t\t\tif (!options.replace || found.live.version === Identity.current.version) return endpoint(found.info);\n\t\t\tyield* Effect.sync(() => options.onStart?.(\"version-mismatch\"));\n\t\t\tyield* terminate(path, found.info.pid);\n\t\t} else {\n\t\t\t// A file nothing answers for, left by a crash, must not mislead the next caller.\n\t\t\tif (found !== undefined && !(yield* alive(found.info.pid))) yield* remove(path);\n\t\t\tyield* Effect.sync(() => options.onStart?.(\"missing\"));\n\t\t}\n\t\treturn yield* start(path, options);\n\t});\n\n/** The registered server once it answers, polled every 200ms for up to `timeout`. */\nconst ready = (path: string, timeout: Duration.Input, pending: Effect.Effect<boolean>) =>\n\tEffect.gen(function* () {\n\t\tconst next = yield* registered(path);\n\t\tif (next?.live !== undefined) return Option.some(endpoint(next.info));\n\t\t// A live registrant that does not answer yet is another caller's server starting up.\n\t\tconst waiting = (yield* pending) || (next !== undefined && (yield* alive(next.info.pid)));\n\t\treturn waiting ? Option.none() : yield* Effect.fail(\"exited\" as const);\n\t}).pipe(\n\t\tEffect.repeat({ until: Option.isSome, schedule: Schedule.spaced(\"200 millis\") }),\n\t\tEffect.timeoutOption(timeout),\n\t\tEffect.map(Option.flatten),\n\t\tEffect.orElseSucceed(() => Option.none<Endpoint>()),\n\t);\n\n/**\n * Start a background server and wait for it to register, quoting the reason\n * from its log when it cannot.\n *\n * The contender stays tied to this scope until it is the server adopted: a\n * timeout, an interrupt, or another caller winning the race stops it rather than\n * leaving an unmanaged server behind.\n */\nconst start = (path: string, options: Options) =>\n\tEffect.gen(function* () {\n\t\tconst contender = yield* spawn(options);\n\t\tconst won = yield* ready(path, START_TIMEOUT, contender.isRunning.pipe(Effect.orElseSucceed(() => false)));\n\t\t// A contender that lost the bind race exits before the winner may have filed its\n\t\t// registration; give the winner a moment before calling the start a failure.\n\t\tconst server = Option.isSome(won) ? won : yield* ready(path, RACE_GRACE, Effect.succeed(true));\n\t\tif (Option.isSome(server)) {\n\t\t\tif (server.value.pid === contender.pid) yield* contender.unref.pipe(Effect.asVoid);\n\t\t\treturn server.value;\n\t\t}\n\n\t\tconst log = Registration.log(options.log);\n\t\tconst reason = yield* lastLine(log);\n\t\treturn yield* new ServerStartError({\n\t\t\tmessage: [\n\t\t\t\t`could not start a CodeWork server on ${options.host}:${options.port}`,\n\t\t\t\t...(reason === undefined ? [] : [`reason: ${reason}`]),\n\t\t\t\t`log: ${log}`,\n\t\t\t\t\"hint: pass --port to use another port, or point `codework tui --server` at a running server\",\n\t\t\t].join(\"\\n\"),\n\t\t});\n\t}).pipe(Effect.scoped);\n\n/**\n * Keep the session's server running for as long as the caller lives.\n *\n * The TUI only reconnects; it cannot start a server. If the one it uses goes\n * away, this brings one back on the same port so the reconnect lands. It never\n * replaces a running server: another invocation may have just upgraded it.\n */\nexport const keepAlive = (initial: Endpoint, options: Options) =>\n\tEffect.gen(function* () {\n\t\tconst current = yield* Ref.make(initial);\n\t\tyield* Effect.gen(function* () {\n\t\t\tconst server = yield* Ref.get(current);\n\t\t\tif (yield* alive(server.pid)) return;\n\t\t\tconst next = yield* ensure({ ...options, port: server.port, replace: false }).pipe(\n\t\t\t\tEffect.orElseSucceed(() => server),\n\t\t\t);\n\t\t\tyield* Ref.set(current, next);\n\t\t}).pipe(Effect.repeat(Schedule.spaced(\"1 second\")));\n\t});\n\n/** Ask `pid` to shut down, escalating to SIGKILL, and retire its registration. */\nconst terminate = (path: string, pid: number) =>\n\tEffect.gen(function* () {\n\t\tconst signal = (name: NodeJS.Signals) => Effect.try(() => process.kill(pid, name)).pipe(Effect.ignore);\n\t\tyield* signal(\"SIGTERM\");\n\t\tif (!(yield* exits(pid, STOP_TIMEOUT))) {\n\t\t\tyield* signal(\"SIGKILL\");\n\t\t\tyield* exits(pid, STOP_TIMEOUT);\n\t\t}\n\t\t// A server removes its own file on a clean exit; this covers the SIGKILL path.\n\t\tconst info = yield* read(path);\n\t\tif (info?.pid === pid) yield* remove(path);\n\t});\n\nconst lastLine = (path: string) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tconst text = yield* fs.readFileString(path).pipe(Effect.orElseSucceed(() => \"\"));\n\t\treturn text\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => line.trim())\n\t\t\t.findLast((line) => line.length > 0);\n\t});\n\n/**\n * Spawn `codework serve --service` detached, inheriting this environment so it\n * resolves the same home, catalog and credentials. It writes its own log, so no\n * stdio is tied to this short-lived caller. It stays referenced, and so stopped\n * with the scope, until `start` adopts it.\n */\nconst spawn = (options: Options) =>\n\tEffect.gen(function* () {\n\t\tconst [file, ...base] = Registration.command();\n\t\tif (file === undefined) return yield* Effect.die(new Error(\"could not resolve the codework entrypoint\"));\n\t\tconst spawner = yield* ChildProcessSpawner.ChildProcessSpawner;\n\t\tconst handle = yield* spawner.spawn(\n\t\t\tChildProcess.make(\n\t\t\t\tfile,\n\t\t\t\t[...base, ...options.args, \"serve\", \"--service\", \"--host\", options.host, \"--port\", String(options.port)],\n\t\t\t\t{ detached: true, stdin: \"ignore\", stdout: \"ignore\", stderr: \"ignore\" },\n\t\t\t),\n\t\t);\n\t\treturn handle;\n\t});\n\nexport * as Discovery from \"./discovery.ts\";\n","import { Global } from \"@codeworksh/harness/effect\";\nimport { Effect, Option } from \"effect\";\nimport type { Discovery } from \"../server/discovery.ts\";\n\ninterface Shared {\n\treadonly userConfigDir: Option.Option<string>;\n\treadonly home: Option.Option<string>;\n\treadonly database: Option.Option<string>;\n}\n\n/** The CodeWork home the shared flags name, resolved the way the harness resolves it. */\nexport const home = (shared: Shared) => Global.resolve(Option.isSome(shared.home) ? { home: shared.home.value } : {});\n\n/**\n * How `tui` and `service` find or start the background server: the home whose\n * `service.json` names it, and the server-owned flags it is started with, in the\n * order `serve` parses them.\n */\nexport const options = (shared: Shared, server: { readonly host: string; readonly port: number }) =>\n\thome(shared).pipe(\n\t\tEffect.map((paths): Discovery.Options => ({\n\t\t\thome: paths.home,\n\t\t\tlog: paths.log,\n\t\t\t...server,\n\t\t\targs: [\n\t\t\t\t...(Option.isSome(shared.home) ? [\"--home\", shared.home.value] : []),\n\t\t\t\t...(Option.isSome(shared.database) ? [\"--database\", shared.database.value] : []),\n\t\t\t\t...(Option.isSome(shared.userConfigDir) ? [\"--user-config-dir\", shared.userConfigDir.value] : []),\n\t\t\t],\n\t\t})),\n\t);\n\nexport * as Service from \"./service.ts\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service-BI_FL26Q.mjs","names":["Client.layer","Client.make","Registration.infoJson","Health.probe","Registration.file","log","Registration.log","Registration.command"],"sources":["../../src/server/health.ts","../../src/server/discovery.ts","../../src/cli/service.ts"],"sourcesContent":["import { Effect, Layer } from \"effect\";\nimport { Client } from \"./client.ts\";\n\n/**\n * One `server.info` call, with the answer normalized to `undefined` for every\n * way a server can fail to answer: unreachable, slow, or speaking a different\n * protocol. A health check that throws where it should say \"no\" would make\n * every caller wrap it in a catch.\n */\nexport const probe = (url: string) =>\n\tEffect.gen(function* () {\n\t\tconst context = yield* Layer.build(Client.layer(url)).pipe(Effect.orElseSucceed(() => undefined));\n\t\tif (context === undefined) return undefined;\n\t\treturn yield* Effect.gen(function* () {\n\t\t\tconst rpc = yield* Client.make;\n\t\t\treturn yield* rpc[\"server.info\"]({}).pipe(Effect.timeout(\"1 second\"));\n\t\t}).pipe(\n\t\t\tEffect.provideContext(context),\n\t\t\tEffect.orElseSucceed(() => undefined),\n\t\t);\n\t}).pipe(Effect.scoped);\n\nexport * as Health from \"./health.ts\";\n","import { type Duration, Effect, FileSystem, Option, Ref, Schedule, Schema } from \"effect\";\nimport { ChildProcess, ChildProcessSpawner } from \"effect/process\";\nimport { Health } from \"./health.ts\";\nimport { Identity } from \"./identity.ts\";\nimport { Registration } from \"./registration.ts\";\n\n/** A server a client can connect to, and the process and port behind it. */\nexport interface Endpoint {\n\treadonly url: string;\n\treadonly pid: number;\n\treadonly port: number;\n}\n\n/** A managed server could not be produced, in the words the CLI prints. */\nexport class ServerStartError extends Schema.TaggedError<ServerStartError>()(\"Discovery.ServerStartError\", {\n\tmessage: Schema.String,\n}) {}\n\nexport interface Options {\n\t/** The CodeWork home whose `service.json` names the server. */\n\treadonly home: string;\n\t/** The home's log directory, where a background server writes `server.log`. */\n\treadonly log: string;\n\treadonly host: string;\n\treadonly port: number;\n\t/** Server-owned flags placed before `serve`. */\n\treadonly args: ReadonlyArray<string>;\n}\n\nconst START_TIMEOUT = \"20 seconds\";\nconst RACE_GRACE = \"2 seconds\";\nconst STOP_TIMEOUT = \"5 seconds\";\n\nconst decode = Schema.decodeUnknownEffect(Registration.infoJson);\n\nconst read = (path: string) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tconst text = yield* fs.readFileString(path).pipe(Effect.option);\n\t\tif (Option.isNone(text)) return undefined;\n\t\treturn yield* decode(text.value).pipe(Effect.option, Effect.map(Option.getOrUndefined));\n\t});\n\nconst remove = (path: string) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tyield* fs.remove(path);\n\t}).pipe(Effect.ignore);\n\n/** Whether `pid` is a running process. EPERM means it exists but belongs to someone else. */\nconst alive = (pid: number) =>\n\tEffect.try({ try: () => process.kill(pid, 0), catch: (error) => (error as NodeJS.ErrnoException).code }).pipe(\n\t\tEffect.as(true),\n\t\tEffect.catch((code) => Effect.succeed(code === \"EPERM\")),\n\t);\n\n/** Whether `pid` exits within `timeout`. */\nconst exits = (pid: number, timeout: typeof STOP_TIMEOUT) =>\n\talive(pid).pipe(\n\t\tEffect.repeat({ while: (running) => running, schedule: Schedule.spaced(\"50 millis\") }),\n\t\tEffect.timeoutOption(timeout),\n\t\tEffect.map(Option.isSome),\n\t);\n\n/**\n * The registered server, with `live` set when the process answering its endpoint\n * is the one that registered. The file is a hint, never proof: a different server\n * on a reused port must not pass for it.\n */\nconst registered = (path: string) =>\n\tEffect.gen(function* () {\n\t\tconst info = yield* read(path);\n\t\tif (info === undefined) return undefined;\n\t\tconst live = yield* Health.probe(info.url);\n\t\treturn { info, live: live?.pid === info.pid ? live : undefined };\n\t});\n\nconst endpoint = (info: Registration.Info): Endpoint => ({ url: info.url, pid: info.pid, port: info.port });\n\n/** The running server registered for `home`, with the build it reports. */\nexport const discover = (home: string) =>\n\tregistered(Registration.file(home)).pipe(\n\t\tEffect.map((found) =>\n\t\t\tfound?.live === undefined ? undefined : { ...endpoint(found.info), version: found.live.version },\n\t\t),\n\t);\n\n/**\n * Stop the server registered for `home`, returning it, or undefined when none\n * answers. Only a server that confirms it is the registrant is signalled: a pid\n * on file may since have been reused by an unrelated process.\n */\nexport const stop = (home: string) =>\n\tEffect.gen(function* () {\n\t\tconst path = Registration.file(home);\n\t\tconst found = yield* registered(path);\n\t\tif (found === undefined) return undefined;\n\t\tif (found.live === undefined) {\n\t\t\tif (!(yield* alive(found.info.pid))) yield* remove(path);\n\t\t\treturn undefined;\n\t\t}\n\t\tyield* terminate(path, found.info.pid);\n\t\treturn endpoint(found.info);\n\t});\n\n/**\n * Ensure a healthy local server for `home`: reuse the registered one, replace it\n * when `replace` is set and it runs a different build, or start a background one.\n *\n * The background server is detached and outlives this caller, so the next\n * invocation reuses it. When two callers race, only the contender that binds the\n * port registers; the loser exits and both adopt the winner, because a caller\n * only ever believes a registration it can confirm over the wire.\n */\nexport const ensure = (\n\toptions: Options & {\n\t\treadonly replace: boolean;\n\t\treadonly onStart?: (reason: \"missing\" | \"version-mismatch\") => void;\n\t},\n) =>\n\tEffect.gen(function* () {\n\t\tconst path = Registration.file(options.home);\n\t\tconst found = yield* registered(path);\n\t\tif (found?.live !== undefined) {\n\t\t\tif (!options.replace || found.live.version === Identity.current.version) return endpoint(found.info);\n\t\t\tyield* Effect.sync(() => options.onStart?.(\"version-mismatch\"));\n\t\t\tyield* terminate(path, found.info.pid);\n\t\t} else {\n\t\t\t// A file nothing answers for, left by a crash, must not mislead the next caller.\n\t\t\tif (found !== undefined && !(yield* alive(found.info.pid))) yield* remove(path);\n\t\t\tyield* Effect.sync(() => options.onStart?.(\"missing\"));\n\t\t}\n\t\treturn yield* start(path, options);\n\t});\n\n/** The registered server once it answers, polled every 200ms for up to `timeout`. */\nconst ready = (path: string, timeout: Duration.Input, pending: Effect.Effect<boolean>) =>\n\tEffect.gen(function* () {\n\t\tconst next = yield* registered(path);\n\t\tif (next?.live !== undefined) return Option.some(endpoint(next.info));\n\t\t// A live registrant that does not answer yet is another caller's server starting up.\n\t\tconst waiting = (yield* pending) || (next !== undefined && (yield* alive(next.info.pid)));\n\t\treturn waiting ? Option.none() : yield* Effect.fail(\"exited\" as const);\n\t}).pipe(\n\t\tEffect.repeat({ until: Option.isSome, schedule: Schedule.spaced(\"200 millis\") }),\n\t\tEffect.timeoutOption(timeout),\n\t\tEffect.map(Option.flatten),\n\t\tEffect.orElseSucceed(() => Option.none<Endpoint>()),\n\t);\n\n/**\n * Start a background server and wait for it to register, quoting the reason\n * from its log when it cannot.\n *\n * The contender stays tied to this scope until it is the server adopted: a\n * timeout, an interrupt, or another caller winning the race stops it rather than\n * leaving an unmanaged server behind.\n */\nconst start = (path: string, options: Options) =>\n\tEffect.gen(function* () {\n\t\tconst contender = yield* spawn(options);\n\t\tconst won = yield* ready(path, START_TIMEOUT, contender.isRunning.pipe(Effect.orElseSucceed(() => false)));\n\t\t// A contender that lost the bind race exits before the winner may have filed its\n\t\t// registration; give the winner a moment before calling the start a failure.\n\t\tconst server = Option.isSome(won) ? won : yield* ready(path, RACE_GRACE, Effect.succeed(true));\n\t\tif (Option.isSome(server)) {\n\t\t\tif (server.value.pid === contender.pid) yield* contender.unref.pipe(Effect.asVoid);\n\t\t\treturn server.value;\n\t\t}\n\n\t\tconst log = Registration.log(options.log);\n\t\tconst reason = yield* lastLine(log);\n\t\treturn yield* new ServerStartError({\n\t\t\tmessage: [\n\t\t\t\t`could not start a CodeWork server on ${options.host}:${options.port}`,\n\t\t\t\t...(reason === undefined ? [] : [`reason: ${reason}`]),\n\t\t\t\t`log: ${log}`,\n\t\t\t\t\"hint: pass --port to use another port, or point `codework tui --server` at a running server\",\n\t\t\t].join(\"\\n\"),\n\t\t});\n\t}).pipe(Effect.scoped);\n\n/**\n * Keep the session's server running for as long as the caller lives.\n *\n * The TUI only reconnects; it cannot start a server. If the one it uses goes\n * away, this brings one back on the same port so the reconnect lands. It never\n * replaces a running server: another invocation may have just upgraded it.\n */\nexport const keepAlive = (initial: Endpoint, options: Options) =>\n\tEffect.gen(function* () {\n\t\tconst current = yield* Ref.make(initial);\n\t\tyield* Effect.gen(function* () {\n\t\t\tconst server = yield* Ref.get(current);\n\t\t\tif (yield* alive(server.pid)) return;\n\t\t\tconst next = yield* ensure({ ...options, port: server.port, replace: false }).pipe(\n\t\t\t\tEffect.orElseSucceed(() => server),\n\t\t\t);\n\t\t\tyield* Ref.set(current, next);\n\t\t}).pipe(Effect.repeat(Schedule.spaced(\"1 second\")));\n\t});\n\n/** Ask `pid` to shut down, escalating to SIGKILL, and retire its registration. */\nconst terminate = (path: string, pid: number) =>\n\tEffect.gen(function* () {\n\t\tconst signal = (name: NodeJS.Signals) => Effect.try(() => process.kill(pid, name)).pipe(Effect.ignore);\n\t\tyield* signal(\"SIGTERM\");\n\t\tif (!(yield* exits(pid, STOP_TIMEOUT))) {\n\t\t\tyield* signal(\"SIGKILL\");\n\t\t\tyield* exits(pid, STOP_TIMEOUT);\n\t\t}\n\t\t// A server removes its own file on a clean exit; this covers the SIGKILL path.\n\t\tconst info = yield* read(path);\n\t\tif (info?.pid === pid) yield* remove(path);\n\t});\n\nconst lastLine = (path: string) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tconst text = yield* fs.readFileString(path).pipe(Effect.orElseSucceed(() => \"\"));\n\t\treturn text\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => line.trim())\n\t\t\t.findLast((line) => line.length > 0);\n\t});\n\n/**\n * Spawn `codework serve --service` detached, inheriting this environment so it\n * resolves the same home, catalog and credentials. It writes its own log, so no\n * stdio is tied to this short-lived caller. It stays referenced, and so stopped\n * with the scope, until `start` adopts it.\n */\nconst spawn = (options: Options) =>\n\tEffect.gen(function* () {\n\t\tconst [file, ...base] = Registration.command();\n\t\tif (file === undefined) return yield* Effect.die(new Error(\"could not resolve the codework entrypoint\"));\n\t\tconst spawner = yield* ChildProcessSpawner.ChildProcessSpawner;\n\t\tconst handle = yield* spawner.spawn(\n\t\t\tChildProcess.make(\n\t\t\t\tfile,\n\t\t\t\t[...base, ...options.args, \"serve\", \"--service\", \"--host\", options.host, \"--port\", String(options.port)],\n\t\t\t\t{ detached: true, stdin: \"ignore\", stdout: \"ignore\", stderr: \"ignore\" },\n\t\t\t),\n\t\t);\n\t\treturn handle;\n\t});\n\nexport * as Discovery from \"./discovery.ts\";\n","import { Global } from \"@codeworksh/harness/effect\";\nimport { Effect, Option } from \"effect\";\nimport type { Discovery } from \"../server/discovery.ts\";\n\ninterface Shared {\n\treadonly userConfigDir: Option.Option<string>;\n\treadonly home: Option.Option<string>;\n\treadonly database: Option.Option<string>;\n}\n\n/** The CodeWork home the shared flags name, resolved the way the harness resolves it. */\nexport const home = (shared: Shared) => Global.resolve(Option.isSome(shared.home) ? { home: shared.home.value } : {});\n\n/**\n * How `tui` and `service` find or start the background server: the home whose\n * `service.json` names it, and the server-owned flags it is started with, in the\n * order `serve` parses them.\n */\nexport const options = (shared: Shared, server: { readonly host: string; readonly port: number }) =>\n\thome(shared).pipe(\n\t\tEffect.map((paths): Discovery.Options => ({\n\t\t\thome: paths.home,\n\t\t\tlog: paths.log,\n\t\t\t...server,\n\t\t\targs: [\n\t\t\t\t...(Option.isSome(shared.home) ? [\"--home\", shared.home.value] : []),\n\t\t\t\t...(Option.isSome(shared.database) ? [\"--database\", shared.database.value] : []),\n\t\t\t\t...(Option.isSome(shared.userConfigDir) ? [\"--user-config-dir\", shared.userConfigDir.value] : []),\n\t\t\t],\n\t\t})),\n\t);\n\nexport * as Service from \"./service.ts\";\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as InvalidInputError } from "./error-
|
|
1
|
+
import { t as InvalidInputError } from "./error-BMI81XvR.mjs";
|
|
2
2
|
import { Effect, FileSystem, Option, Path } from "effect";
|
|
3
3
|
import { Global, Plugin, Settings } from "@codeworksh/harness/effect";
|
|
4
4
|
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
@@ -218,4 +218,4 @@ const resolveTarget = Effect.fn("CLI.plugin.resolveTarget")(function* (shared, g
|
|
|
218
218
|
//#endregion
|
|
219
219
|
export { resolveTarget as a, withPluginsLock as c, readPlugins as i, writePlugins as l, matches as n, spellings as o, matching as r, userLayer as s, identify as t, written as u };
|
|
220
220
|
|
|
221
|
-
//# sourceMappingURL=settings-
|
|
221
|
+
//# sourceMappingURL=settings-BfmoL27m.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-BM_gw7Fm.mjs","names":["parseJsonc"],"sources":["../../src/cli/cmd/handlers/plugin/settings.ts"],"sourcesContent":["import { Global, Plugin, Settings } from \"@codeworksh/harness/effect\";\nimport { Effect, FileSystem, Option, Path } from \"effect\";\nimport { applyEdits, modify, parse as parseJsonc, type ParseError } from \"jsonc-parser\";\nimport { InvalidInputError } from \"../../../error.ts\";\n\n/**\n * Reading and rewriting the `plugins` array of one settings file.\n *\n * Edits go through `jsonc-parser`, so a hand-written file keeps its comments, key order and\n * formatting: a plugin added by the CLI must not reformat a file its owner maintains. The write\n * is a temp file renamed over the original, so an interrupted run leaves the previous settings\n * intact rather than a half-written document.\n */\n\nexport interface Shared {\n\treadonly userConfigDir: Option.Option<string>;\n\treadonly home: Option.Option<string>;\n}\n\n/**\n * The user layer a command reads and writes: `--user-config-dir` when given -- a hard override of\n * the home's settings file -- resolved, like every app-level flag, against the directory the\n * command runs in (`hostCwd`), never against a linked session's `hostDir`.\n */\nexport const userLayer = (shared: Shared, hostCwd: string) =>\n\tOption.isNone(shared.userConfigDir)\n\t\t? {}\n\t\t: { userConfigDir: Settings.userConfigDir(shared.userConfigDir.value, hostCwd) };\n\nexport interface Target {\n\treadonly path: string;\n\t/** Set when this command had to create the project directory, so the caller can say so. */\n\treadonly created?: string;\n\t/** The project this file belongs to, which a local entry is written relative to. */\n\treadonly root?: string;\n}\n\n/** The module an entry names: a bare loader string, or the `package` of a config object. */\nconst moduleOf = (entry: unknown): string | undefined => {\n\tif (typeof entry === \"string\") return entry;\n\tif (typeof entry !== \"object\" || entry === null) return undefined;\n\tconst { package: located } = entry as { package?: unknown };\n\treturn typeof located === \"string\" ? located : undefined;\n};\n\n/** The plugin ID an entry configures, if it names one. */\nconst idOf = (entry: unknown): string | undefined => {\n\tif (typeof entry !== \"object\" || entry === null) return undefined;\n\tconst { plugin } = entry as { plugin?: unknown };\n\treturn typeof plugin === \"string\" ? plugin : undefined;\n};\n\n/**\n * Every spelling a reference answers to, without touching the filesystem.\n *\n * `canonical` drops a version suffix and resolves a relative path against the file that declared\n * it, so `@acme/x@1.2.0` and `@acme/x` are one name. The literal string is kept alongside it\n * because a plugin ID is not a module reference and survives `canonical` unchanged.\n */\nconst aliases = (reference: string, directory: string): Effect.Effect<ReadonlyArray<string>> =>\n\tPlugin.canonical(reference, directory).pipe(\n\t\tEffect.map((canonical) => [reference, canonical] as ReadonlyArray<string>),\n\t\t// A reference that does not parse still answers to itself: a plugin ID is not a module\n\t\t// reference, and it has to keep matching an entry that names it.\n\t\tEffect.orElseSucceed(() => [reference] as ReadonlyArray<string>),\n\t);\n\n/** Every spelling `reference` answers to, computed once for a whole command. */\nexport const spellings = (reference: string, directory: string): Effect.Effect<ReadonlySet<string>> =>\n\tEffect.map(aliases(reference, directory), (found) => new Set(found));\n\nexport interface Entry {\n\treadonly value: unknown;\n\treadonly answers: ReadonlySet<string>;\n\treadonly loads: boolean;\n}\n\n/**\n * What each configured entry answers to.\n *\n * Comparing the string as written is not enough. `plugin add @acme/x@1.2.0` records a version the\n * user will not repeat when removing it, and one plugin can be named three ways: by package, by\n * path, or by the ID it declares -- a `{ \"plugin\": \"<id>\" }` configuration object sits next to the\n * module entry that loaded it, and removing the plugin has to take both.\n *\n * The declared ID is only knowable by importing the module, so it is read from what is already\n * installed: `resolveCached` reads a published package out of the harness cache and never runs an\n * installer, so editing a settings file cannot fetch anything. An entry that does not resolve --\n * uninstalled, moved, or broken -- keeps the names it was written with rather than failing the\n * command, because an unusable plugin is exactly the one a user is trying to remove.\n */\nexport const identify = Effect.fn(\"CLI.plugin.identify\")(function* (\n\tplugins: ReadonlyArray<unknown>,\n\tfile: string,\n\tcache: string,\n) {\n\tconst nodePath = yield* Path.Path;\n\tconst directory = nodePath.dirname(file);\n\tconst entries: Entry[] = [];\n\tfor (const value of plugins) {\n\t\tconst answers = new Set<string>();\n\t\tconst id = idOf(value);\n\t\tif (id !== undefined) answers.add(id);\n\t\tconst module = moduleOf(value);\n\t\tif (module !== undefined) {\n\t\t\tfor (const alias of yield* aliases(module, directory)) answers.add(alias);\n\t\t\tconst declared = yield* Plugin.inspect(module, {\n\t\t\t\tcache,\n\t\t\t\thostDir: directory,\n\t\t\t\tfile,\n\t\t\t\tinstall: Plugin.resolveCached,\n\t\t\t}).pipe(Effect.option);\n\t\t\tif (Option.isSome(declared)) {\n\t\t\t\tanswers.add(declared.value.id);\n\t\t\t\tif (declared.value.name !== undefined) answers.add(declared.value.name);\n\t\t\t}\n\t\t}\n\t\tentries.push({ value, answers, loads: typeof value === \"string\" });\n\t}\n\treturn entries as ReadonlyArray<Entry>;\n});\n\n/**\n * What a local reference should be *written* as, which is rarely what was typed.\n *\n * A relative path on the command line is relative to the command's host directory; in a settings file it is relative to\n * that file's own directory. Those differ the moment the command runs anywhere but the directory\n * holding the file, and writing the string verbatim produces a silently wrong entry:\n *\n * ```\n * $ cd ~/workspace/app/packages/fubar\n * $ codework plugin add ./plugins/x.ts # means packages/fubar/plugins/x.ts\n * → the project is ~/workspace/app, so the entry lands in app/.codework/settings.jsonc\n * → written verbatim, the harness loads app/.codework/plugins/x.ts ← a different file\n * ```\n *\n * With both ends inside one project the entry is written relative to the file, which is portable\n * to any checkout of that repository and therefore worth committing. Otherwise -- a `-g` write\n * into `<home>`, a `--user-config-dir` elsewhere, or a target outside the project -- relative\n * would be a lie, and an absolute path means the same thing read from anywhere.\n *\n * A package spec is returned untouched: it names no location.\n */\nexport const written = Effect.fn(\"CLI.plugin.written\")(function* (\n\treference: string,\n\tinput: { readonly hostDir: string; readonly file: string; readonly root: string | undefined },\n) {\n\tconst nodePath = yield* Path.Path;\n\tconst target = yield* Plugin.parse(reference, input.hostDir).pipe(Effect.option);\n\tif (Option.isNone(target) || target.value.kind !== \"local\") return reference;\n\n\tconst absolute = target.value.path;\n\tconst inside = (root: string, path: string) => path === root || path.startsWith(`${root}${nodePath.sep}`);\n\tif (input.root !== undefined && inside(input.root, input.file) && inside(input.root, absolute)) {\n\t\tconst relative = nodePath.relative(nodePath.dirname(input.file), absolute);\n\t\treturn relative.startsWith(\"..\") ? relative : `./${relative}`;\n\t}\n\treturn absolute;\n});\n\n/** `reference` selects `entry` when any spelling of one is a spelling of the other. */\nexport const matches = (entry: Entry, spelled: ReadonlySet<string>) =>\n\t[...spelled].some((alias) => entry.answers.has(alias));\n\n/** Every entry connected to a reference through a package spelling or declared plugin ID. */\nexport const matching = (entries: ReadonlyArray<Entry>, spelled: ReadonlySet<string>) => {\n\tconst answers = new Set(spelled);\n\tconst found = new Set<Entry>();\n\tfor (;;) {\n\t\tconst next = entries.filter(\n\t\t\t(entry) => !found.has(entry) && [...entry.answers].some((answer) => answers.has(answer)),\n\t\t);\n\t\tif (next.length === 0) return found as ReadonlySet<Entry>;\n\t\tfor (const entry of next) {\n\t\t\tfound.add(entry);\n\t\t\tfor (const answer of entry.answers) answers.add(answer);\n\t\t}\n\t}\n};\n\nexport const readPlugins = Effect.fn(\"CLI.plugin.readPlugins\")(function* (path: string) {\n\tconst fs = yield* FileSystem.FileSystem;\n\tconst exists = yield* fs.exists(path);\n\tconst source = exists ? yield* fs.readFileString(path) : \"{}\";\n\tconst errors: ParseError[] = [];\n\tconst document: unknown = parseJsonc(source, errors, { allowTrailingComma: true });\n\tif (errors.length > 0 || typeof document !== \"object\" || document === null || Array.isArray(document)) {\n\t\treturn yield* new InvalidInputError({ message: `${path} is not a valid settings file` });\n\t}\n\tconst plugins = (document as { plugins?: unknown }).plugins;\n\tif (plugins !== undefined && !Array.isArray(plugins)) {\n\t\treturn yield* new InvalidInputError({ message: `${path}: \"plugins\" must be an array` });\n\t}\n\treturn { source, plugins: (plugins ?? []) as ReadonlyArray<unknown> };\n});\n\nexport const writePlugins = Effect.fn(\"CLI.plugin.writePlugins\")(function* (\n\tpath: string,\n\tsource: string,\n\tplugins: ReadonlyArray<unknown>,\n) {\n\tconst fs = yield* FileSystem.FileSystem;\n\tconst nodePath = yield* Path.Path;\n\tconst edited = applyEdits(\n\t\tsource,\n\t\tmodify(source, [\"plugins\"], plugins, { formattingOptions: { tabSize: 2, insertSpaces: false } }),\n\t);\n\tconst directory = nodePath.dirname(path);\n\tyield* fs.makeDirectory(directory, { recursive: true });\n\tconst mode = (yield* fs.exists(path)) ? (yield* fs.stat(path)).mode & 0o777 : 0o600;\n\tconst temporary = yield* fs.makeTempFile({ directory, prefix: `.${nodePath.basename(path)}.`, suffix: \".tmp\" });\n\tyield* fs\n\t\t.writeFileString(temporary, edited.endsWith(\"\\n\") ? edited : `${edited}\\n`)\n\t\t.pipe(\n\t\t\tEffect.andThen(fs.chmod(temporary, mode)),\n\t\t\tEffect.andThen(fs.rename(temporary, path)),\n\t\t\tEffect.ensuring(fs.remove(temporary, { force: true }).pipe(Effect.ignore)),\n\t\t);\n});\n\n/**\n * Serialize one settings read-modify-rename across CLI processes.\n *\n * The install or inspection that precedes an edit stays outside this scope; only the local edit is\n * locked, so unrelated downloads do not wait behind one another.\n */\nexport const withPluginsLock = <A, E, R>(path: string, edit: Effect.Effect<A, E, R>) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tconst nodePath = yield* Path.Path;\n\t\tyield* fs.makeDirectory(nodePath.dirname(path), { recursive: true });\n\t\tyield* Plugin.settingsLock(\n\t\t\t`${path}.lock`,\n\t\t\t() => new InvalidInputError({ message: `timed out waiting to edit ${path}` }),\n\t\t);\n\t\treturn yield* edit;\n\t}).pipe(Effect.scoped);\n\n/**\n * Which file the edit lands in.\n *\n * A plugin belongs to a project by default — it is part of how that repository is worked on, so\n * the entry belongs in a file the repository can commit. `--global` writes the user settings file\n * instead, the way `npm -g` installs outside a package: `<--home>/settings.jsonc`, or the\n * `--user-config-dir` one that replaces it.\n *\n * The project is found the way the harness finds it: the nearest ancestor holding a `.codework`\n * directory, so a command run from `packages/app` edits the repository's own file rather than\n * creating a second one beside it. When no ancestor has one, the project begins here -- the\n * directory is created and the caller is told, because that decision changes where every future\n * plugin entry lands, and a mistyped `cd` is the way it goes wrong.\n *\n * There is nothing to warn about between layers: plugin entries from every layer accumulate, so a\n * user-wide entry still applies inside a project that declares its own.\n */\nexport const resolveTarget = Effect.fn(\"CLI.plugin.resolveTarget\")(function* (\n\tshared: Shared,\n\tglobal: boolean,\n\t/**\n\t * Where to discover the project from, when it is not the shell's directory.\n\t *\n\t * A session's host directory is the only other honest answer: it is a declared host path. The\n\t * session's *space* is not, and neither is its project -- a `Project` is env-independent by\n\t * design, so it has no host directory to offer.\n\t */\n\tfrom?: string,\n\t/** Only `plugin add` may establish a project marker when no project exists yet. */\n\tcreate = true,\n) {\n\tconst fs = yield* FileSystem.FileSystem;\n\tconst nodePath = yield* Path.Path;\n\tconst hostCwd = nodePath.resolve(\".\");\n\t// The linked session's host directory, or -- with none -- the one the command runs in.\n\tconst hostDir = from ?? hostCwd;\n\tconst home = yield* Global.resolve(Option.isNone(shared.home) ? {} : { home: shared.home.value });\n\tconst root = global ? undefined : yield* Settings.projectRoot(hostDir, home.home);\n\t// `paths` is ordered lowest priority first: the user layer, then the project.\n\tconst groups = Settings.paths({\n\t\thome: home.home,\n\t\t...(root === undefined ? {} : { root }),\n\t\t...userLayer(shared, hostCwd),\n\t});\n\tconst chosen = global ? 0 : 1;\n\t// An empty group is the project layer saying there is no project above this directory. Then\n\t// this directory becomes one -- reported, not silent, because the marker shadows any outer\n\t// project from now on, so a `.codework/` created by a mistyped `cd` is worth spotting at once.\n\tconst found = groups[chosen] ?? [];\n\tif (found.length > 0) {\n\t\tconst existing = yield* Effect.findFirst(found, (candidate) => fs.exists(candidate));\n\t\treturn {\n\t\t\tpath: Option.getOrElse(existing, () => found[0]!),\n\t\t\t...(root === undefined ? {} : { root }),\n\t\t} satisfies Target;\n\t}\n\tconst marker = nodePath.join(hostDir, \".codework\");\n\t// `created` is reported either way: with `create` unset the marker is named, not made, so a\n\t// caller can say what its write is about to establish without establishing it early.\n\tif (!create)\n\t\treturn { path: nodePath.join(marker, \"settings.jsonc\"), created: marker, root: hostDir } satisfies Target;\n\tyield* fs.makeDirectory(marker, { recursive: true });\n\treturn { path: nodePath.join(marker, \"settings.jsonc\"), created: marker, root: hostDir } satisfies Target;\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"settings-BfmoL27m.mjs","names":["parseJsonc"],"sources":["../../src/cli/cmd/handlers/plugin/settings.ts"],"sourcesContent":["import { Global, Plugin, Settings } from \"@codeworksh/harness/effect\";\nimport { Effect, FileSystem, Option, Path } from \"effect\";\nimport { applyEdits, modify, parse as parseJsonc, type ParseError } from \"jsonc-parser\";\nimport { InvalidInputError } from \"../../../error.ts\";\n\n/**\n * Reading and rewriting the `plugins` array of one settings file.\n *\n * Edits go through `jsonc-parser`, so a hand-written file keeps its comments, key order and\n * formatting: a plugin added by the CLI must not reformat a file its owner maintains. The write\n * is a temp file renamed over the original, so an interrupted run leaves the previous settings\n * intact rather than a half-written document.\n */\n\nexport interface Shared {\n\treadonly userConfigDir: Option.Option<string>;\n\treadonly home: Option.Option<string>;\n}\n\n/**\n * The user layer a command reads and writes: `--user-config-dir` when given -- a hard override of\n * the home's settings file -- resolved, like every app-level flag, against the directory the\n * command runs in (`hostCwd`), never against a linked session's `hostDir`.\n */\nexport const userLayer = (shared: Shared, hostCwd: string) =>\n\tOption.isNone(shared.userConfigDir)\n\t\t? {}\n\t\t: { userConfigDir: Settings.userConfigDir(shared.userConfigDir.value, hostCwd) };\n\nexport interface Target {\n\treadonly path: string;\n\t/** Set when this command had to create the project directory, so the caller can say so. */\n\treadonly created?: string;\n\t/** The project this file belongs to, which a local entry is written relative to. */\n\treadonly root?: string;\n}\n\n/** The module an entry names: a bare loader string, or the `package` of a config object. */\nconst moduleOf = (entry: unknown): string | undefined => {\n\tif (typeof entry === \"string\") return entry;\n\tif (typeof entry !== \"object\" || entry === null) return undefined;\n\tconst { package: located } = entry as { package?: unknown };\n\treturn typeof located === \"string\" ? located : undefined;\n};\n\n/** The plugin ID an entry configures, if it names one. */\nconst idOf = (entry: unknown): string | undefined => {\n\tif (typeof entry !== \"object\" || entry === null) return undefined;\n\tconst { plugin } = entry as { plugin?: unknown };\n\treturn typeof plugin === \"string\" ? plugin : undefined;\n};\n\n/**\n * Every spelling a reference answers to, without touching the filesystem.\n *\n * `canonical` drops a version suffix and resolves a relative path against the file that declared\n * it, so `@acme/x@1.2.0` and `@acme/x` are one name. The literal string is kept alongside it\n * because a plugin ID is not a module reference and survives `canonical` unchanged.\n */\nconst aliases = (reference: string, directory: string): Effect.Effect<ReadonlyArray<string>> =>\n\tPlugin.canonical(reference, directory).pipe(\n\t\tEffect.map((canonical) => [reference, canonical] as ReadonlyArray<string>),\n\t\t// A reference that does not parse still answers to itself: a plugin ID is not a module\n\t\t// reference, and it has to keep matching an entry that names it.\n\t\tEffect.orElseSucceed(() => [reference] as ReadonlyArray<string>),\n\t);\n\n/** Every spelling `reference` answers to, computed once for a whole command. */\nexport const spellings = (reference: string, directory: string): Effect.Effect<ReadonlySet<string>> =>\n\tEffect.map(aliases(reference, directory), (found) => new Set(found));\n\nexport interface Entry {\n\treadonly value: unknown;\n\treadonly answers: ReadonlySet<string>;\n\treadonly loads: boolean;\n}\n\n/**\n * What each configured entry answers to.\n *\n * Comparing the string as written is not enough. `plugin add @acme/x@1.2.0` records a version the\n * user will not repeat when removing it, and one plugin can be named three ways: by package, by\n * path, or by the ID it declares -- a `{ \"plugin\": \"<id>\" }` configuration object sits next to the\n * module entry that loaded it, and removing the plugin has to take both.\n *\n * The declared ID is only knowable by importing the module, so it is read from what is already\n * installed: `resolveCached` reads a published package out of the harness cache and never runs an\n * installer, so editing a settings file cannot fetch anything. An entry that does not resolve --\n * uninstalled, moved, or broken -- keeps the names it was written with rather than failing the\n * command, because an unusable plugin is exactly the one a user is trying to remove.\n */\nexport const identify = Effect.fn(\"CLI.plugin.identify\")(function* (\n\tplugins: ReadonlyArray<unknown>,\n\tfile: string,\n\tcache: string,\n) {\n\tconst nodePath = yield* Path.Path;\n\tconst directory = nodePath.dirname(file);\n\tconst entries: Entry[] = [];\n\tfor (const value of plugins) {\n\t\tconst answers = new Set<string>();\n\t\tconst id = idOf(value);\n\t\tif (id !== undefined) answers.add(id);\n\t\tconst module = moduleOf(value);\n\t\tif (module !== undefined) {\n\t\t\tfor (const alias of yield* aliases(module, directory)) answers.add(alias);\n\t\t\tconst declared = yield* Plugin.inspect(module, {\n\t\t\t\tcache,\n\t\t\t\thostDir: directory,\n\t\t\t\tfile,\n\t\t\t\tinstall: Plugin.resolveCached,\n\t\t\t}).pipe(Effect.option);\n\t\t\tif (Option.isSome(declared)) {\n\t\t\t\tanswers.add(declared.value.id);\n\t\t\t\tif (declared.value.name !== undefined) answers.add(declared.value.name);\n\t\t\t}\n\t\t}\n\t\tentries.push({ value, answers, loads: typeof value === \"string\" });\n\t}\n\treturn entries as ReadonlyArray<Entry>;\n});\n\n/**\n * What a local reference should be *written* as, which is rarely what was typed.\n *\n * A relative path on the command line is relative to the command's host directory; in a settings file it is relative to\n * that file's own directory. Those differ the moment the command runs anywhere but the directory\n * holding the file, and writing the string verbatim produces a silently wrong entry:\n *\n * ```\n * $ cd ~/workspace/app/packages/fubar\n * $ codework plugin add ./plugins/x.ts # means packages/fubar/plugins/x.ts\n * → the project is ~/workspace/app, so the entry lands in app/.codework/settings.jsonc\n * → written verbatim, the harness loads app/.codework/plugins/x.ts ← a different file\n * ```\n *\n * With both ends inside one project the entry is written relative to the file, which is portable\n * to any checkout of that repository and therefore worth committing. Otherwise -- a `-g` write\n * into `<home>`, a `--user-config-dir` elsewhere, or a target outside the project -- relative\n * would be a lie, and an absolute path means the same thing read from anywhere.\n *\n * A package spec is returned untouched: it names no location.\n */\nexport const written = Effect.fn(\"CLI.plugin.written\")(function* (\n\treference: string,\n\tinput: { readonly hostDir: string; readonly file: string; readonly root: string | undefined },\n) {\n\tconst nodePath = yield* Path.Path;\n\tconst target = yield* Plugin.parse(reference, input.hostDir).pipe(Effect.option);\n\tif (Option.isNone(target) || target.value.kind !== \"local\") return reference;\n\n\tconst absolute = target.value.path;\n\tconst inside = (root: string, path: string) => path === root || path.startsWith(`${root}${nodePath.sep}`);\n\tif (input.root !== undefined && inside(input.root, input.file) && inside(input.root, absolute)) {\n\t\tconst relative = nodePath.relative(nodePath.dirname(input.file), absolute);\n\t\treturn relative.startsWith(\"..\") ? relative : `./${relative}`;\n\t}\n\treturn absolute;\n});\n\n/** `reference` selects `entry` when any spelling of one is a spelling of the other. */\nexport const matches = (entry: Entry, spelled: ReadonlySet<string>) =>\n\t[...spelled].some((alias) => entry.answers.has(alias));\n\n/** Every entry connected to a reference through a package spelling or declared plugin ID. */\nexport const matching = (entries: ReadonlyArray<Entry>, spelled: ReadonlySet<string>) => {\n\tconst answers = new Set(spelled);\n\tconst found = new Set<Entry>();\n\tfor (;;) {\n\t\tconst next = entries.filter(\n\t\t\t(entry) => !found.has(entry) && [...entry.answers].some((answer) => answers.has(answer)),\n\t\t);\n\t\tif (next.length === 0) return found as ReadonlySet<Entry>;\n\t\tfor (const entry of next) {\n\t\t\tfound.add(entry);\n\t\t\tfor (const answer of entry.answers) answers.add(answer);\n\t\t}\n\t}\n};\n\nexport const readPlugins = Effect.fn(\"CLI.plugin.readPlugins\")(function* (path: string) {\n\tconst fs = yield* FileSystem.FileSystem;\n\tconst exists = yield* fs.exists(path);\n\tconst source = exists ? yield* fs.readFileString(path) : \"{}\";\n\tconst errors: ParseError[] = [];\n\tconst document: unknown = parseJsonc(source, errors, { allowTrailingComma: true });\n\tif (errors.length > 0 || typeof document !== \"object\" || document === null || Array.isArray(document)) {\n\t\treturn yield* new InvalidInputError({ message: `${path} is not a valid settings file` });\n\t}\n\tconst plugins = (document as { plugins?: unknown }).plugins;\n\tif (plugins !== undefined && !Array.isArray(plugins)) {\n\t\treturn yield* new InvalidInputError({ message: `${path}: \"plugins\" must be an array` });\n\t}\n\treturn { source, plugins: (plugins ?? []) as ReadonlyArray<unknown> };\n});\n\nexport const writePlugins = Effect.fn(\"CLI.plugin.writePlugins\")(function* (\n\tpath: string,\n\tsource: string,\n\tplugins: ReadonlyArray<unknown>,\n) {\n\tconst fs = yield* FileSystem.FileSystem;\n\tconst nodePath = yield* Path.Path;\n\tconst edited = applyEdits(\n\t\tsource,\n\t\tmodify(source, [\"plugins\"], plugins, { formattingOptions: { tabSize: 2, insertSpaces: false } }),\n\t);\n\tconst directory = nodePath.dirname(path);\n\tyield* fs.makeDirectory(directory, { recursive: true });\n\tconst mode = (yield* fs.exists(path)) ? (yield* fs.stat(path)).mode & 0o777 : 0o600;\n\tconst temporary = yield* fs.makeTempFile({ directory, prefix: `.${nodePath.basename(path)}.`, suffix: \".tmp\" });\n\tyield* fs\n\t\t.writeFileString(temporary, edited.endsWith(\"\\n\") ? edited : `${edited}\\n`)\n\t\t.pipe(\n\t\t\tEffect.andThen(fs.chmod(temporary, mode)),\n\t\t\tEffect.andThen(fs.rename(temporary, path)),\n\t\t\tEffect.ensuring(fs.remove(temporary, { force: true }).pipe(Effect.ignore)),\n\t\t);\n});\n\n/**\n * Serialize one settings read-modify-rename across CLI processes.\n *\n * The install or inspection that precedes an edit stays outside this scope; only the local edit is\n * locked, so unrelated downloads do not wait behind one another.\n */\nexport const withPluginsLock = <A, E, R>(path: string, edit: Effect.Effect<A, E, R>) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tconst nodePath = yield* Path.Path;\n\t\tyield* fs.makeDirectory(nodePath.dirname(path), { recursive: true });\n\t\tyield* Plugin.settingsLock(\n\t\t\t`${path}.lock`,\n\t\t\t() => new InvalidInputError({ message: `timed out waiting to edit ${path}` }),\n\t\t);\n\t\treturn yield* edit;\n\t}).pipe(Effect.scoped);\n\n/**\n * Which file the edit lands in.\n *\n * A plugin belongs to a project by default — it is part of how that repository is worked on, so\n * the entry belongs in a file the repository can commit. `--global` writes the user settings file\n * instead, the way `npm -g` installs outside a package: `<--home>/settings.jsonc`, or the\n * `--user-config-dir` one that replaces it.\n *\n * The project is found the way the harness finds it: the nearest ancestor holding a `.codework`\n * directory, so a command run from `packages/app` edits the repository's own file rather than\n * creating a second one beside it. When no ancestor has one, the project begins here -- the\n * directory is created and the caller is told, because that decision changes where every future\n * plugin entry lands, and a mistyped `cd` is the way it goes wrong.\n *\n * There is nothing to warn about between layers: plugin entries from every layer accumulate, so a\n * user-wide entry still applies inside a project that declares its own.\n */\nexport const resolveTarget = Effect.fn(\"CLI.plugin.resolveTarget\")(function* (\n\tshared: Shared,\n\tglobal: boolean,\n\t/**\n\t * Where to discover the project from, when it is not the shell's directory.\n\t *\n\t * A session's host directory is the only other honest answer: it is a declared host path. The\n\t * session's *space* is not, and neither is its project -- a `Project` is env-independent by\n\t * design, so it has no host directory to offer.\n\t */\n\tfrom?: string,\n\t/** Only `plugin add` may establish a project marker when no project exists yet. */\n\tcreate = true,\n) {\n\tconst fs = yield* FileSystem.FileSystem;\n\tconst nodePath = yield* Path.Path;\n\tconst hostCwd = nodePath.resolve(\".\");\n\t// The linked session's host directory, or -- with none -- the one the command runs in.\n\tconst hostDir = from ?? hostCwd;\n\tconst home = yield* Global.resolve(Option.isNone(shared.home) ? {} : { home: shared.home.value });\n\tconst root = global ? undefined : yield* Settings.projectRoot(hostDir, home.home);\n\t// `paths` is ordered lowest priority first: the user layer, then the project.\n\tconst groups = Settings.paths({\n\t\thome: home.home,\n\t\t...(root === undefined ? {} : { root }),\n\t\t...userLayer(shared, hostCwd),\n\t});\n\tconst chosen = global ? 0 : 1;\n\t// An empty group is the project layer saying there is no project above this directory. Then\n\t// this directory becomes one -- reported, not silent, because the marker shadows any outer\n\t// project from now on, so a `.codework/` created by a mistyped `cd` is worth spotting at once.\n\tconst found = groups[chosen] ?? [];\n\tif (found.length > 0) {\n\t\tconst existing = yield* Effect.findFirst(found, (candidate) => fs.exists(candidate));\n\t\treturn {\n\t\t\tpath: Option.getOrElse(existing, () => found[0]!),\n\t\t\t...(root === undefined ? {} : { root }),\n\t\t} satisfies Target;\n\t}\n\tconst marker = nodePath.join(hostDir, \".codework\");\n\t// `created` is reported either way: with `create` unset the marker is named, not made, so a\n\t// caller can say what its write is about to establish without establishing it early.\n\tif (!create)\n\t\treturn { path: nodePath.join(marker, \"settings.jsonc\"), created: marker, root: hostDir } satisfies Target;\n\tyield* fs.makeDirectory(marker, { recursive: true });\n\treturn { path: nodePath.join(marker, \"settings.jsonc\"), created: marker, root: hostDir } satisfies Target;\n});\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as Cmd } from "./runtime-B43doHjV.mjs";
|
|
2
|
-
import { f as layer, n as OAuthError, t as InvalidInputError } from "./error-
|
|
2
|
+
import { f as layer, n as OAuthError, t as InvalidInputError } from "./error-BMI81XvR.mjs";
|
|
3
3
|
import { Effect, Option, Path } from "effect";
|
|
4
4
|
import { Global } from "@codeworksh/harness/effect";
|
|
5
5
|
import { checkOAuthProvider, oauthProviderLabel, summarizeGitHubCopilot, summarizeOpenAICodex } from "@codeworksh/aikit/oauth/summary";
|
|
@@ -113,4 +113,4 @@ const withServer = (url, program) => program.pipe(Effect.provide(layer(url)), Ef
|
|
|
113
113
|
//#endregion
|
|
114
114
|
export { oauthProviderLabel as a, makeOps as i, attempt as n, selectProvider as o, localOps as r, withServer as s, assertServerScope as t };
|
|
115
115
|
|
|
116
|
-
//# sourceMappingURL=shared-
|
|
116
|
+
//# sourceMappingURL=shared-CnuCjvcG.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared-
|
|
1
|
+
{"version":3,"file":"shared-CnuCjvcG.mjs","names":["Client.layer"],"sources":["../../src/cli/cmd/handlers/auth/shared.ts"],"sourcesContent":["/* @effect-diagnostics nodeBuiltinImport:off -- OAuth login opens the system browser. */\n/* @effect-diagnostics globalDate:off -- Stored expiry timestamps are rendered for the user. */\n/* oxlint-disable effecttsgo/async-function -- Aikit OAuth callbacks require Promise-returning functions. */\nimport {\n\tGitHubCopilotOAuthClient,\n\tJsonGitHubCopilotAuthStorage,\n\ttype GitHubCopilotAuthStorage,\n} from \"@codeworksh/aikit/oauth/github/copilot\";\nimport {\n\tJsonOpenAICodexAuthStorage,\n\tOpenAICodexOAuthClient,\n\ttype OpenAICodexAuthStorage,\n} from \"@codeworksh/aikit/oauth/openai/codex\";\nimport { openBrowser, promptLine } from \"@codeworksh/aikit/oauth/interactive\";\nimport {\n\tcheckOAuthProvider,\n\toauthProviderLabel,\n\tsummarizeGitHubCopilot,\n\tsummarizeOpenAICodex,\n\ttype OAuthSummary,\n} from \"@codeworksh/aikit/oauth/summary\";\nimport { Global } from \"@codeworksh/harness/effect\";\nimport { Effect, Option, Path } from \"effect\";\nimport { Client } from \"../../../../server/client.ts\";\nimport type { Contract } from \"../../../../server/contract.ts\";\nimport { InvalidInputError, OAuthError } from \"../../../error.ts\";\nimport { Cmd } from \"../../cmd.ts\";\n\nexport type Provider = Contract.OAuthProvider;\n\n// The wire shape and aikit's summary are the same record.\nexport type Summary = Contract.OAuthInfo & OAuthSummary;\n\nconst announce = (\n\tinfo: { url: string; userCode?: string | undefined; instructions?: string | undefined },\n\tbrowser: boolean,\n): void => {\n\tprocess.stderr.write(\n\t\t`${info.instructions ?? \"Complete authentication in your browser.\"}\\n${\n\t\t\tinfo.userCode === undefined ? \"\" : `Enter code: ${info.userCode}\\n`\n\t\t}${info.url}\\n`,\n\t);\n\tif (browser) {\n\t\tvoid openBrowser(info.url, (error) => void process.stderr.write(`Failed to open browser: ${error.message}\\n`));\n\t}\n};\n\nexport interface LoginOptions {\n\treadonly browser: boolean;\n\treadonly device: boolean;\n\treadonly enterprise: Option.Option<string>;\n\treadonly enableModels: boolean;\n}\n\nexport interface Ops {\n\treadonly path: string;\n\treadonly login: (options: LoginOptions) => Promise<{ wire: Contract.OAuthCredentials; summary: Summary }>;\n\treadonly stored: () => Promise<Summary | undefined>;\n\treadonly refreshed: () => Promise<Summary | undefined>;\n\treadonly logout: () => Promise<void>;\n}\n\n/** Holds nothing: a server's credentials live on the server, not on this disk. */\nconst transient: OpenAICodexAuthStorage & GitHubCopilotAuthStorage = {\n\tget: async () => undefined,\n\tset: async () => {},\n\tclear: async () => {},\n};\n\nconst codexOps = (storage: OpenAICodexAuthStorage, path: string): Ops => {\n\tconst client = new OpenAICodexOAuthClient({ storage });\n\treturn {\n\t\tpath,\n\t\tlogin: async (options) => {\n\t\t\tconst credentials = await client.login({\n\t\t\t\tdevice: options.device,\n\t\t\t\tonAuth: (info) => announce(info, options.browser),\n\t\t\t\tonProgress: (message) => void process.stderr.write(`${message}\\n`),\n\t\t\t\tonPrompt: (prompt) => promptLine(prompt.message),\n\t\t\t});\n\t\t\treturn { wire: { provider: \"openai-codex\", ...credentials }, summary: summarizeOpenAICodex(credentials) };\n\t\t},\n\t\tstored: async () => {\n\t\t\tconst credentials = await storage.get();\n\t\t\treturn credentials && summarizeOpenAICodex(credentials);\n\t\t},\n\t\trefreshed: async () => {\n\t\t\tconst credentials = await client.getCredentials();\n\t\t\treturn credentials && summarizeOpenAICodex(credentials);\n\t\t},\n\t\tlogout: () => client.logout(),\n\t};\n};\n\nconst copilotOps = (storage: GitHubCopilotAuthStorage, path: string): Ops => {\n\tconst client = new GitHubCopilotOAuthClient({ storage });\n\tconst read = async () => {\n\t\tconst credentials = await storage.get();\n\t\treturn credentials && summarizeGitHubCopilot(credentials);\n\t};\n\treturn {\n\t\tpath,\n\t\tlogin: async (options) => {\n\t\t\tconst credentials = await client.login({\n\t\t\t\t...(Option.isSome(options.enterprise) && { enterpriseUrl: options.enterprise.value }),\n\t\t\t\tenableModels: options.enableModels,\n\t\t\t\tonAuth: (info) => announce(info, options.browser),\n\t\t\t\tonProgress: (message) => void process.stderr.write(`${message}\\n`),\n\t\t\t});\n\t\t\treturn { wire: { provider: \"github-copilot\", ...credentials }, summary: summarizeGitHubCopilot(credentials) };\n\t\t},\n\t\tstored: read,\n\t\t// GitHub OAuth tokens never expire, so there is nothing to refresh.\n\t\trefreshed: read,\n\t\tlogout: () => client.logout(),\n\t};\n};\n\nexport const makeOps = (provider: Provider, options: { path?: string | undefined; persist?: boolean } = {}): Ops => {\n\tconst file = options.path === undefined ? {} : { path: options.path };\n\tif (provider === \"openai-codex\") {\n\t\tconst storage = options.persist === false ? undefined : new JsonOpenAICodexAuthStorage(file);\n\t\treturn codexOps(storage ?? transient, storage?.path ?? \"\");\n\t}\n\tconst storage = options.persist === false ? undefined : new JsonGitHubCopilotAuthStorage(file);\n\treturn copilotOps(storage ?? transient, storage?.path ?? \"\");\n};\n\nexport const attempt = <A>(message: string, run: () => Promise<A>) =>\n\tEffect.tryPromise({ try: run, catch: (cause) => new OAuthError({ message, cause }) });\n\n/** The provider named by the two mutually exclusive flags. */\nexport const selectProvider = (flags: {\n\treadonly openaiCodex: boolean;\n\treadonly githubCopilot: boolean;\n}): Effect.Effect<Provider, InvalidInputError> => {\n\tconst checked = checkOAuthProvider(flags);\n\treturn checked.ok\n\t\t? Effect.succeed(checked.provider)\n\t\t: Effect.fail(new InvalidInputError({ message: checked.message }));\n};\n\n/** Reject the local-credential flags when the credentials live on a server. */\nexport const assertServerScope = (authFile: Option.Option<string>) =>\n\tEffect.gen(function* () {\n\t\tconst shared = yield* Cmd.spec;\n\t\tif (Option.isSome(authFile) || Option.isSome(shared.home)) {\n\t\t\treturn yield* new InvalidInputError({\n\t\t\t\tmessage: \"--auth-file and --home belong on codework serve when using --server\",\n\t\t\t});\n\t\t}\n\t});\n\n/** Credential operations against this machine's auth.json. */\nexport const localOps = (provider: Provider, authFile: Option.Option<string>) =>\n\tEffect.gen(function* () {\n\t\tconst shared = yield* Cmd.spec;\n\t\tconst path = yield* Path.Path;\n\t\tconst file = Option.isSome(authFile)\n\t\t\t? authFile.value\n\t\t\t: Option.isSome(shared.home)\n\t\t\t\t? path.join((yield* Global.resolve({ home: shared.home.value })).home, \"aikit\", \"auth.json\")\n\t\t\t\t: undefined;\n\t\treturn makeOps(provider, file === undefined ? {} : { path: file });\n\t});\n\n/** Run an effect against the RPC server at `url`, discharging the client. */\nexport const withServer = <A, E, R>(url: string, program: Effect.Effect<A, E, R>) =>\n\tprogram.pipe(Effect.provide(Client.layer(url)), Effect.scoped);\n\nexport { Client, oauthProviderLabel };\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as Cmd, t as handler } from "./runtime-B43doHjV.mjs";
|
|
2
|
-
import { d as writeOut, i as reportFailure } from "./error-
|
|
3
|
-
import { i as ensure, n as options } from "./service-
|
|
2
|
+
import { d as writeOut, i as reportFailure } from "./error-BMI81XvR.mjs";
|
|
3
|
+
import { i as ensure, n as options } from "./service-BI_FL26Q.mjs";
|
|
4
4
|
import { Effect } from "effect";
|
|
5
5
|
//#region src/cli/cmd/handlers/service/start.ts
|
|
6
6
|
var start_default = handler(Cmd.commands.service.commands.start, Effect.fn("CLI.service.start")(function* ({ host, port }) {
|
|
@@ -17,4 +17,4 @@ var start_default = handler(Cmd.commands.service.commands.start, Effect.fn("CLI.
|
|
|
17
17
|
//#endregion
|
|
18
18
|
export { start_default as default };
|
|
19
19
|
|
|
20
|
-
//# sourceMappingURL=start-
|
|
20
|
+
//# sourceMappingURL=start-Bkk-Y6yE.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-
|
|
1
|
+
{"version":3,"file":"start-Bkk-Y6yE.mjs","names":["Runtime.handler","options","Service.options","Discovery.ensure"],"sources":["../../src/cli/cmd/handlers/service/start.ts"],"sourcesContent":["import { Effect } from \"effect\";\nimport { Runtime } from \"../../../../framework/runtime.ts\";\nimport { Discovery } from \"../../../../server/discovery.ts\";\nimport { reportFailure } from \"../../../error.ts\";\nimport { writeOut } from \"../../../output.ts\";\nimport { Service } from \"../../../service.ts\";\nimport { Cmd } from \"../../cmd.ts\";\n\nexport default Runtime.handler(\n\tCmd.commands.service.commands.start,\n\tEffect.fn(\"CLI.service.start\")(function* ({ host, port }) {\n\t\tconst options = yield* Service.options(yield* Cmd.spec, { host, port });\n\t\tconst server = yield* Discovery.ensure({ ...options, replace: true });\n\t\tyield* writeOut(`${server.url}\\n`);\n\t}, Effect.catch(reportFailure)),\n);\n"],"mappings":";;;;;AAQA,IAAA,gBAAeA,QACd,IAAI,SAAS,QAAQ,SAAS,OAC9B,OAAO,GAAG,mBAAmB,CAAC,CAAC,WAAW,EAAE,MAAM,QAAQ;CACzD,MAAMC,YAAU,OAAOC,QAAgB,OAAO,IAAI,MAAM;EAAE;EAAM;CAAK,CAAC;CACtE,MAAM,SAAS,OAAOC,OAAiB;EAAE,GAAGF;EAAS,SAAS;CAAK,CAAC;CACpE,OAAO,SAAS,GAAG,OAAO,IAAI,GAAG;AAClC,GAAG,OAAO,MAAM,aAAa,CAAC,CAC/B"}
|