lalph 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -10
- package/dist/cli.mjs +120 -22
- package/package.json +1 -1
- package/src/Prd.ts +8 -4
- package/src/PromptGen.ts +1 -8
- package/src/Runner.ts +8 -1
- package/src/Worktree.ts +59 -0
- package/src/cli.ts +75 -8
package/README.md
CHANGED
|
@@ -6,22 +6,14 @@ A small CLI that connects to Linear, pulls the next set of unstarted issues into
|
|
|
6
6
|
|
|
7
7
|
- Install dependencies: `pnpm install`
|
|
8
8
|
- Build the CLI: `pnpm build`
|
|
9
|
+
- Add `.lalph/` to `.gitignore` to keep local state private
|
|
9
10
|
|
|
10
11
|
## CLI usage
|
|
11
12
|
|
|
12
13
|
- Run the main loop: `npx -y lalph@latest`
|
|
14
|
+
- Run multiple iterations with concurrency: `npx -y lalph@latest --iterations 4 --concurrency 2`
|
|
13
15
|
- Select a Linear project: `npx -y lalph@latest select-project`
|
|
14
16
|
- Select a label filter: `npx -y lalph@latest select-label`
|
|
15
17
|
- Select a CLI agent: `npx -y lalph@latest select-agent`
|
|
16
18
|
|
|
17
19
|
The first run opens a Linear OAuth flow and stores the token locally.
|
|
18
|
-
|
|
19
|
-
## Generated files
|
|
20
|
-
|
|
21
|
-
- `.lalph/prd.json`: synced task list pulled from Linear; update task states here.
|
|
22
|
-
- `PROGRESS.md`: append-only log of work completed by the agent.
|
|
23
|
-
- `.lalph/config`: local key-value store for Linear tokens and user selections.
|
|
24
|
-
|
|
25
|
-
## Checks
|
|
26
|
-
|
|
27
|
-
- Type check + format: `pnpm check`
|
package/dist/cli.mjs
CHANGED
|
@@ -8270,8 +8270,8 @@ const whileLoop$1 = /* @__PURE__ */ makePrimitive$1({
|
|
|
8270
8270
|
/** @internal */
|
|
8271
8271
|
const forEach$2 = /* @__PURE__ */ dual((args$1) => typeof args$1[1] === "function", (iterable, f, options) => withFiber$1((parent) => {
|
|
8272
8272
|
const concurrencyOption = options?.concurrency === "inherit" ? parent.getRef(CurrentConcurrency) : options?.concurrency ?? 1;
|
|
8273
|
-
const concurrency = concurrencyOption === "unbounded" ? Number.POSITIVE_INFINITY : Math.max(1, concurrencyOption);
|
|
8274
|
-
if (concurrency === 1) return forEachSequential(iterable, f, options);
|
|
8273
|
+
const concurrency$1 = concurrencyOption === "unbounded" ? Number.POSITIVE_INFINITY : Math.max(1, concurrencyOption);
|
|
8274
|
+
if (concurrency$1 === 1) return forEachSequential(iterable, f, options);
|
|
8275
8275
|
const items = fromIterable$2(iterable);
|
|
8276
8276
|
let length = items.length;
|
|
8277
8277
|
if (length === 0) return options?.discard ? void_$3 : succeed$4([]);
|
|
@@ -8288,7 +8288,7 @@ const forEach$2 = /* @__PURE__ */ dual((args$1) => typeof args$1[1] === "functio
|
|
|
8288
8288
|
let interrupted = false;
|
|
8289
8289
|
function pump() {
|
|
8290
8290
|
pumping = true;
|
|
8291
|
-
while (inProgress < concurrency && index < length) {
|
|
8291
|
+
while (inProgress < concurrency$1 && index < length) {
|
|
8292
8292
|
const currentIndex = index;
|
|
8293
8293
|
const item = items[currentIndex];
|
|
8294
8294
|
index++;
|
|
@@ -8312,7 +8312,7 @@ const forEach$2 = /* @__PURE__ */ dual((args$1) => typeof args$1[1] === "functio
|
|
|
8312
8312
|
doneCount++;
|
|
8313
8313
|
inProgress--;
|
|
8314
8314
|
if (doneCount === length) resume(failures.length > 0 ? exitFailCause(causeFromFailures(failures)) : succeed$4(out));
|
|
8315
|
-
else if (!pumping && !failed && inProgress < concurrency) pump();
|
|
8315
|
+
else if (!pumping && !failed && inProgress < concurrency$1) pump();
|
|
8316
8316
|
});
|
|
8317
8317
|
} catch (err) {
|
|
8318
8318
|
failed = true;
|
|
@@ -18354,6 +18354,35 @@ const logWithLevel = logWithLevel$1;
|
|
|
18354
18354
|
*/
|
|
18355
18355
|
const log$1 = /* @__PURE__ */ logWithLevel$1();
|
|
18356
18356
|
/**
|
|
18357
|
+
* Logs one or more messages at the WARNING level.
|
|
18358
|
+
*
|
|
18359
|
+
* @example
|
|
18360
|
+
* ```ts
|
|
18361
|
+
* import { Effect } from "effect"
|
|
18362
|
+
*
|
|
18363
|
+
* const program = Effect.gen(function*() {
|
|
18364
|
+
* yield* Effect.logWarning("API rate limit approaching")
|
|
18365
|
+
* yield* Effect.logWarning("Retries remaining:", 2, "Operation:", "fetchData")
|
|
18366
|
+
*
|
|
18367
|
+
* // Useful for non-critical issues
|
|
18368
|
+
* const deprecated = true
|
|
18369
|
+
* if (deprecated) {
|
|
18370
|
+
* yield* Effect.logWarning("Using deprecated API endpoint")
|
|
18371
|
+
* }
|
|
18372
|
+
* })
|
|
18373
|
+
*
|
|
18374
|
+
* Effect.runPromise(program)
|
|
18375
|
+
* // Output:
|
|
18376
|
+
* // timestamp=2023-... level=WARN message="API rate limit approaching"
|
|
18377
|
+
* // timestamp=2023-... level=WARN message="Retries remaining: 2 Operation: fetchData"
|
|
18378
|
+
* // timestamp=2023-... level=WARN message="Using deprecated API endpoint"
|
|
18379
|
+
* ```
|
|
18380
|
+
*
|
|
18381
|
+
* @since 2.0.0
|
|
18382
|
+
* @category logging
|
|
18383
|
+
*/
|
|
18384
|
+
const logWarning = /* @__PURE__ */ logWithLevel$1("Warn");
|
|
18385
|
+
/**
|
|
18357
18386
|
* Logs one or more messages at the ERROR level.
|
|
18358
18387
|
*
|
|
18359
18388
|
* @example
|
|
@@ -53211,12 +53240,12 @@ var require_limiter = /* @__PURE__ */ __commonJSMin$1(((exports, module) => {
|
|
|
53211
53240
|
* @param {Number} [concurrency=Infinity] The maximum number of jobs allowed
|
|
53212
53241
|
* to run concurrently
|
|
53213
53242
|
*/
|
|
53214
|
-
constructor(concurrency) {
|
|
53243
|
+
constructor(concurrency$1) {
|
|
53215
53244
|
this[kDone] = () => {
|
|
53216
53245
|
this.pending--;
|
|
53217
53246
|
this[kRun]();
|
|
53218
53247
|
};
|
|
53219
|
-
this.concurrency = concurrency || Infinity;
|
|
53248
|
+
this.concurrency = concurrency$1 || Infinity;
|
|
53220
53249
|
this.jobs = [];
|
|
53221
53250
|
this.pending = 0;
|
|
53222
53251
|
}
|
|
@@ -132176,9 +132205,42 @@ const teamSelect = fnUntraced(function* (project) {
|
|
|
132176
132205
|
yield* selectedTeamId.set(some(teamId));
|
|
132177
132206
|
});
|
|
132178
132207
|
|
|
132208
|
+
//#endregion
|
|
132209
|
+
//#region src/Worktree.ts
|
|
132210
|
+
var Worktree = class extends Service()("lalph/Worktree", { make: gen(function* () {
|
|
132211
|
+
const fs = yield* FileSystem;
|
|
132212
|
+
const pathService = yield* Path$1;
|
|
132213
|
+
const directory = yield* fs.makeTempDirectory();
|
|
132214
|
+
yield* addFinalizer(fnUntraced(function* () {
|
|
132215
|
+
yield* execIgnore(make$21`git worktree remove ${directory}`);
|
|
132216
|
+
}));
|
|
132217
|
+
yield* exec(make$21`git worktree add ${directory} -d HEAD`);
|
|
132218
|
+
yield* fs.makeDirectory(pathService.join(directory, ".lalph"), { recursive: true });
|
|
132219
|
+
yield* forEach$1([
|
|
132220
|
+
make$21({
|
|
132221
|
+
cwd: directory,
|
|
132222
|
+
extendEnv: true,
|
|
132223
|
+
shell: process.env.SHELL ?? true
|
|
132224
|
+
})`direnv allow`,
|
|
132225
|
+
make$21({
|
|
132226
|
+
cwd: directory,
|
|
132227
|
+
extendEnv: true,
|
|
132228
|
+
shell: process.env.SHELL ?? true
|
|
132229
|
+
})`devenv allow`,
|
|
132230
|
+
make$21({ cwd: directory })`git submodule update --init --recursive`
|
|
132231
|
+
], execIgnore, { concurrency: "unbounded" });
|
|
132232
|
+
return { directory };
|
|
132233
|
+
}) }) {
|
|
132234
|
+
static layer = effect(this, this.make);
|
|
132235
|
+
};
|
|
132236
|
+
const exec = (command) => command.asEffect().pipe(flatMap((proc) => proc.exitCode), scoped$1);
|
|
132237
|
+
const execIgnore = (command) => command.asEffect().pipe(flatMap((proc) => proc.exitCode), catchCause$1(logWarning), scoped$1);
|
|
132238
|
+
|
|
132179
132239
|
//#endregion
|
|
132180
132240
|
//#region src/Prd.ts
|
|
132181
132241
|
var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
|
|
132242
|
+
const worktree = yield* Worktree;
|
|
132243
|
+
const pathService = yield* Path$1;
|
|
132182
132244
|
const fs = yield* FileSystem;
|
|
132183
132245
|
const linear = yield* Linear;
|
|
132184
132246
|
const project = yield* CurrentProject;
|
|
@@ -132190,7 +132252,7 @@ var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
|
|
|
132190
132252
|
} })).pipe(runCollect).pipe(map$4(PrdList.fromLinearIssues));
|
|
132191
132253
|
if (initial.issues.size === 0) return yield* new NoMoreWork({});
|
|
132192
132254
|
const current = yield* make$22(initial);
|
|
132193
|
-
const prdFile = `.lalph/prd.json
|
|
132255
|
+
const prdFile = pathService.join(worktree.directory, `.lalph/prd.json`);
|
|
132194
132256
|
yield* fs.writeFileString(prdFile, initial.toJson());
|
|
132195
132257
|
const sync$2 = gen(function* () {
|
|
132196
132258
|
const json = yield* fs.readFileString(prdFile);
|
|
@@ -132224,13 +132286,12 @@ var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
|
|
|
132224
132286
|
capacity: 1,
|
|
132225
132287
|
strategy: "dropping"
|
|
132226
132288
|
}), runForEach(() => ignore(sync$2)), forkScoped);
|
|
132227
|
-
return {
|
|
132289
|
+
return {
|
|
132290
|
+
current,
|
|
132291
|
+
path: prdFile
|
|
132292
|
+
};
|
|
132228
132293
|
}) }) {
|
|
132229
|
-
static layer = effect(this, this.make).pipe(provide$2([
|
|
132230
|
-
Linear.layer,
|
|
132231
|
-
Settings.layer,
|
|
132232
|
-
CurrentProject.layer
|
|
132233
|
-
]));
|
|
132294
|
+
static layer = effect(this, this.make).pipe(provide$2([CurrentProject.layer, Worktree.layer]));
|
|
132234
132295
|
};
|
|
132235
132296
|
var NoMoreWork = class extends ErrorClass("lalph/Prd/NoMoreWork")({ _tag: tag("NoMoreWork") }) {
|
|
132236
132297
|
message = "No more work to be done!";
|
|
@@ -132293,8 +132354,6 @@ var PrdList = class PrdList extends Class$1 {
|
|
|
132293
132354
|
//#region src/PromptGen.ts
|
|
132294
132355
|
var PromptGen = class extends Service()("lalph/PromptGen", { make: gen(function* () {
|
|
132295
132356
|
const linear = yield* Linear;
|
|
132296
|
-
const fs = yield* FileSystem;
|
|
132297
|
-
yield* scoped$1(fs.open("PROGRESS.md", { flag: "a+" }));
|
|
132298
132357
|
return { prompt: `# Instructions
|
|
132299
132358
|
|
|
132300
132359
|
1. Decide which single task to work on next from the prd.json file. This should
|
|
@@ -132349,6 +132408,7 @@ ${JSON.stringify(PrdIssue.jsonSchema, null, 2)}
|
|
|
132349
132408
|
//#endregion
|
|
132350
132409
|
//#region src/Runner.ts
|
|
132351
132410
|
const run = gen(function* () {
|
|
132411
|
+
const worktree = yield* Worktree;
|
|
132352
132412
|
const promptGen = yield* PromptGen;
|
|
132353
132413
|
const cliCommand = (yield* getOrSelectCliAgent).command({
|
|
132354
132414
|
prompt: promptGen.prompt,
|
|
@@ -132356,13 +132416,18 @@ const run = gen(function* () {
|
|
|
132356
132416
|
progressFilePath: "PROGRESS.md"
|
|
132357
132417
|
});
|
|
132358
132418
|
const exitCode = yield* (yield* make$21(cliCommand[0], cliCommand.slice(1), {
|
|
132419
|
+
cwd: worktree.directory,
|
|
132359
132420
|
extendEnv: true,
|
|
132360
132421
|
stdout: "inherit",
|
|
132361
132422
|
stderr: "inherit",
|
|
132362
132423
|
stdin: "inherit"
|
|
132363
132424
|
})).exitCode;
|
|
132364
132425
|
yield* log$1(`Agent exited with code: ${exitCode}`);
|
|
132365
|
-
}).pipe(scoped$1, provide([
|
|
132426
|
+
}).pipe(scoped$1, provide([
|
|
132427
|
+
PromptGen.layer,
|
|
132428
|
+
Prd.layer,
|
|
132429
|
+
Worktree.layer
|
|
132430
|
+
]));
|
|
132366
132431
|
const selectCliAgent = gen(function* () {
|
|
132367
132432
|
const agent = yield* select({
|
|
132368
132433
|
message: "Select the CLI agent to use",
|
|
@@ -132392,18 +132457,51 @@ const selectLabel = make$26("select-label").pipe(withDescription("Select the lab
|
|
|
132392
132457
|
onNone: () => "No Label",
|
|
132393
132458
|
onSome: (l) => l.name
|
|
132394
132459
|
})}`);
|
|
132395
|
-
}
|
|
132460
|
+
})));
|
|
132396
132461
|
const selectAgent = make$26("select-agent").pipe(withDescription("Select the CLI agent to use"), withHandler(() => selectCliAgent));
|
|
132397
|
-
const iterations = integer("iterations").pipe(withAlias("i"), withDefault(
|
|
132398
|
-
const
|
|
132399
|
-
|
|
132400
|
-
|
|
132462
|
+
const iterations = integer("iterations").pipe(withDescription$1("Number of iterations to run, defaults to unlimited"), withAlias("i"), withDefault(Number.POSITIVE_INFINITY));
|
|
132463
|
+
const concurrency = integer("concurrency").pipe(withDescription$1("Number of concurrent agents, defaults to 1"), withAlias("c"), withDefault(1));
|
|
132464
|
+
const root = make$26("lalph", {
|
|
132465
|
+
iterations,
|
|
132466
|
+
concurrency
|
|
132467
|
+
}).pipe(withHandler(fnUntraced(function* ({ iterations: iterations$1, concurrency: concurrency$1 }) {
|
|
132468
|
+
const isFinite$3 = Number.isFinite(iterations$1);
|
|
132469
|
+
const iterationsDisplay = isFinite$3 ? iterations$1 : "unlimited";
|
|
132470
|
+
const runConcurrency = Math.max(1, concurrency$1);
|
|
132471
|
+
const semaphore = makeSemaphoreUnsafe(runConcurrency);
|
|
132472
|
+
yield* log$1(`Executing ${iterationsDisplay} iteration(s) with concurrency ${runConcurrency}`);
|
|
132473
|
+
let iteration = 0;
|
|
132474
|
+
let lastStartedAt = makeUnsafe$3(0);
|
|
132475
|
+
let inProgress = 0;
|
|
132476
|
+
while (true) {
|
|
132477
|
+
yield* semaphore.take(1);
|
|
132478
|
+
if (isFinite$3 && iteration >= iterations$1) break;
|
|
132479
|
+
const currentIteration = iteration;
|
|
132480
|
+
if (inProgress > 0) {
|
|
132481
|
+
const nextEarliestStart = lastStartedAt.pipe(add$1({ seconds: 30 }));
|
|
132482
|
+
const diff = distance(yield* now, nextEarliestStart);
|
|
132483
|
+
if (diff > 0) yield* sleep(diff);
|
|
132484
|
+
}
|
|
132485
|
+
lastStartedAt = yield* now;
|
|
132486
|
+
inProgress++;
|
|
132487
|
+
yield* run.pipe(catchTag("NoMoreWork", (e) => {
|
|
132488
|
+
if (isFinite$3) {
|
|
132489
|
+
iterations$1 = currentIteration;
|
|
132490
|
+
return fail$3(e);
|
|
132491
|
+
}
|
|
132492
|
+
return log$1("No more work to process, waiting 30 seconds...").pipe(andThen(sleep("30 seconds")));
|
|
132493
|
+
}), catchCause$1(logWarning), annotateLogs({ iteration: currentIteration }), ensuring(suspend$2(() => {
|
|
132494
|
+
inProgress--;
|
|
132495
|
+
return semaphore.release(1);
|
|
132496
|
+
})), forkChild);
|
|
132497
|
+
iteration++;
|
|
132498
|
+
}
|
|
132401
132499
|
})), withSubcommands([
|
|
132402
132500
|
selectProject,
|
|
132403
132501
|
selectLabel,
|
|
132404
132502
|
selectAgent
|
|
132405
132503
|
]));
|
|
132406
|
-
run$1(root, { version: "0.1.0" }).pipe(provide(Settings.layer.pipe(provideMerge(layer$1))), runMain);
|
|
132504
|
+
run$1(root, { version: "0.1.0" }).pipe(provide(mergeAll(Settings.layer, Linear.layer).pipe(provideMerge(layer$1))), runMain);
|
|
132407
132505
|
|
|
132408
132506
|
//#endregion
|
|
132409
132507
|
export { };
|
package/package.json
CHANGED
package/src/Prd.ts
CHANGED
|
@@ -4,17 +4,21 @@ import {
|
|
|
4
4
|
FileSystem,
|
|
5
5
|
Layer,
|
|
6
6
|
Option,
|
|
7
|
+
Path,
|
|
7
8
|
Schema,
|
|
8
9
|
ServiceMap,
|
|
9
10
|
Stream,
|
|
10
11
|
SubscriptionRef,
|
|
11
12
|
} from "effect"
|
|
12
13
|
import { CurrentProject, Linear } from "./Linear.ts"
|
|
13
|
-
import { selectedLabelId, selectedTeamId
|
|
14
|
+
import { selectedLabelId, selectedTeamId } from "./Settings.ts"
|
|
14
15
|
import type { Issue } from "@linear/sdk"
|
|
16
|
+
import { Worktree } from "./Worktree.ts"
|
|
15
17
|
|
|
16
18
|
export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
17
19
|
make: Effect.gen(function* () {
|
|
20
|
+
const worktree = yield* Worktree
|
|
21
|
+
const pathService = yield* Path.Path
|
|
18
22
|
const fs = yield* FileSystem.FileSystem
|
|
19
23
|
const linear = yield* Linear
|
|
20
24
|
const project = yield* CurrentProject
|
|
@@ -46,7 +50,7 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
|
46
50
|
|
|
47
51
|
const current = yield* SubscriptionRef.make(initial)
|
|
48
52
|
|
|
49
|
-
const prdFile = `.lalph/prd.json`
|
|
53
|
+
const prdFile = pathService.join(worktree.directory, `.lalph/prd.json`)
|
|
50
54
|
|
|
51
55
|
yield* fs.writeFileString(prdFile, initial.toJson())
|
|
52
56
|
|
|
@@ -97,11 +101,11 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
|
|
|
97
101
|
Effect.forkScoped,
|
|
98
102
|
)
|
|
99
103
|
|
|
100
|
-
return { current } as const
|
|
104
|
+
return { current, path: prdFile } as const
|
|
101
105
|
}),
|
|
102
106
|
}) {
|
|
103
107
|
static layer = Layer.effect(this, this.make).pipe(
|
|
104
|
-
Layer.provide([
|
|
108
|
+
Layer.provide([CurrentProject.layer, Worktree.layer]),
|
|
105
109
|
)
|
|
106
110
|
}
|
|
107
111
|
|
package/src/PromptGen.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect,
|
|
1
|
+
import { Effect, Layer, ServiceMap } from "effect"
|
|
2
2
|
import { Linear } from "./Linear.ts"
|
|
3
3
|
import { PrdIssue } from "./Prd.ts"
|
|
4
4
|
|
|
@@ -7,13 +7,6 @@ export class PromptGen extends ServiceMap.Service<PromptGen>()(
|
|
|
7
7
|
{
|
|
8
8
|
make: Effect.gen(function* () {
|
|
9
9
|
const linear = yield* Linear
|
|
10
|
-
const fs = yield* FileSystem.FileSystem
|
|
11
|
-
|
|
12
|
-
yield* Effect.scoped(
|
|
13
|
-
fs.open("PROGRESS.md", {
|
|
14
|
-
flag: "a+",
|
|
15
|
-
}),
|
|
16
|
-
)
|
|
17
10
|
|
|
18
11
|
const prompt = `# Instructions
|
|
19
12
|
|
package/src/Runner.ts
CHANGED
|
@@ -5,16 +5,20 @@ import { ChildProcess } from "effect/unstable/process"
|
|
|
5
5
|
import { Prompt } from "effect/unstable/cli"
|
|
6
6
|
import { allCliAgents } from "./domain/CliAgent.ts"
|
|
7
7
|
import { selectedCliAgentId } from "./Settings.ts"
|
|
8
|
+
import { Worktree } from "./Worktree.ts"
|
|
8
9
|
|
|
9
10
|
export const run = Effect.gen(function* () {
|
|
11
|
+
const worktree = yield* Worktree
|
|
10
12
|
const promptGen = yield* PromptGen
|
|
11
13
|
const cliAgent = yield* getOrSelectCliAgent
|
|
14
|
+
|
|
12
15
|
const cliCommand = cliAgent.command({
|
|
13
16
|
prompt: promptGen.prompt,
|
|
14
17
|
prdFilePath: ".lalph/prd.json",
|
|
15
18
|
progressFilePath: "PROGRESS.md",
|
|
16
19
|
})
|
|
17
20
|
const command = ChildProcess.make(cliCommand[0]!, cliCommand.slice(1), {
|
|
21
|
+
cwd: worktree.directory,
|
|
18
22
|
extendEnv: true,
|
|
19
23
|
stdout: "inherit",
|
|
20
24
|
stderr: "inherit",
|
|
@@ -25,7 +29,10 @@ export const run = Effect.gen(function* () {
|
|
|
25
29
|
const exitCode = yield* agent.exitCode
|
|
26
30
|
|
|
27
31
|
yield* Effect.log(`Agent exited with code: ${exitCode}`)
|
|
28
|
-
}).pipe(
|
|
32
|
+
}).pipe(
|
|
33
|
+
Effect.scoped,
|
|
34
|
+
Effect.provide([PromptGen.layer, Prd.layer, Worktree.layer]),
|
|
35
|
+
)
|
|
29
36
|
|
|
30
37
|
export const selectCliAgent = Effect.gen(function* () {
|
|
31
38
|
const agent = yield* Prompt.select({
|
package/src/Worktree.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Effect, FileSystem, Layer, Path, ServiceMap } from "effect"
|
|
2
|
+
import { ChildProcess } from "effect/unstable/process"
|
|
3
|
+
|
|
4
|
+
export class Worktree extends ServiceMap.Service<Worktree>()("lalph/Worktree", {
|
|
5
|
+
make: Effect.gen(function* () {
|
|
6
|
+
const fs = yield* FileSystem.FileSystem
|
|
7
|
+
const pathService = yield* Path.Path
|
|
8
|
+
const directory = yield* fs.makeTempDirectory()
|
|
9
|
+
|
|
10
|
+
yield* Effect.addFinalizer(
|
|
11
|
+
Effect.fnUntraced(function* () {
|
|
12
|
+
yield* execIgnore(ChildProcess.make`git worktree remove ${directory}`)
|
|
13
|
+
}),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
yield* exec(ChildProcess.make`git worktree add ${directory} -d HEAD`)
|
|
17
|
+
|
|
18
|
+
yield* fs.makeDirectory(pathService.join(directory, ".lalph"), {
|
|
19
|
+
recursive: true,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
yield* Effect.forEach(
|
|
23
|
+
[
|
|
24
|
+
ChildProcess.make({
|
|
25
|
+
cwd: directory,
|
|
26
|
+
extendEnv: true,
|
|
27
|
+
shell: process.env.SHELL ?? true,
|
|
28
|
+
})`direnv allow`,
|
|
29
|
+
ChildProcess.make({
|
|
30
|
+
cwd: directory,
|
|
31
|
+
extendEnv: true,
|
|
32
|
+
shell: process.env.SHELL ?? true,
|
|
33
|
+
})`devenv allow`,
|
|
34
|
+
ChildProcess.make({
|
|
35
|
+
cwd: directory,
|
|
36
|
+
})`git submodule update --init --recursive`,
|
|
37
|
+
],
|
|
38
|
+
execIgnore,
|
|
39
|
+
{ concurrency: "unbounded" },
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return { directory } as const
|
|
43
|
+
}),
|
|
44
|
+
}) {
|
|
45
|
+
static layer = Layer.effect(this, this.make)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const exec = (command: ChildProcess.Command) =>
|
|
49
|
+
command.asEffect().pipe(
|
|
50
|
+
Effect.flatMap((proc) => proc.exitCode),
|
|
51
|
+
Effect.scoped,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const execIgnore = (command: ChildProcess.Command) =>
|
|
55
|
+
command.asEffect().pipe(
|
|
56
|
+
Effect.flatMap((proc) => proc.exitCode),
|
|
57
|
+
Effect.catchCause(Effect.logWarning),
|
|
58
|
+
Effect.scoped,
|
|
59
|
+
)
|
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command, Flag } from "effect/unstable/cli"
|
|
4
|
-
import { Effect, Layer, Option } from "effect"
|
|
4
|
+
import { DateTime, Effect, Layer, Option } from "effect"
|
|
5
5
|
import { NodeRuntime, NodeServices } from "@effect/platform-node"
|
|
6
6
|
import { CurrentProject, labelSelect, Linear } from "./Linear.ts"
|
|
7
7
|
import { layerKvs } from "./Kvs.ts"
|
|
@@ -34,7 +34,7 @@ const selectLabel = Command.make("select-label").pipe(
|
|
|
34
34
|
onSome: (l) => l.name,
|
|
35
35
|
})}`,
|
|
36
36
|
)
|
|
37
|
-
}
|
|
37
|
+
}),
|
|
38
38
|
),
|
|
39
39
|
)
|
|
40
40
|
|
|
@@ -44,17 +44,80 @@ const selectAgent = Command.make("select-agent").pipe(
|
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
const iterations = Flag.integer("iterations").pipe(
|
|
47
|
+
Flag.withDescription("Number of iterations to run, defaults to unlimited"),
|
|
47
48
|
Flag.withAlias("i"),
|
|
49
|
+
Flag.withDefault(Number.POSITIVE_INFINITY),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const concurrency = Flag.integer("concurrency").pipe(
|
|
53
|
+
Flag.withDescription("Number of concurrent agents, defaults to 1"),
|
|
54
|
+
Flag.withAlias("c"),
|
|
48
55
|
Flag.withDefault(1),
|
|
49
56
|
)
|
|
50
57
|
|
|
51
|
-
const root = Command.make("lalph", { iterations }).pipe(
|
|
58
|
+
const root = Command.make("lalph", { iterations, concurrency }).pipe(
|
|
52
59
|
Command.withHandler(
|
|
53
|
-
Effect.fnUntraced(function* ({ iterations }) {
|
|
54
|
-
|
|
60
|
+
Effect.fnUntraced(function* ({ iterations, concurrency }) {
|
|
61
|
+
const isFinite = Number.isFinite(iterations)
|
|
62
|
+
const iterationsDisplay = isFinite ? iterations : "unlimited"
|
|
63
|
+
const runConcurrency = Math.max(1, concurrency)
|
|
64
|
+
const semaphore = Effect.makeSemaphoreUnsafe(runConcurrency)
|
|
65
|
+
|
|
66
|
+
yield* Effect.log(
|
|
67
|
+
`Executing ${iterationsDisplay} iteration(s) with concurrency ${runConcurrency}`,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
let iteration = 0
|
|
71
|
+
let lastStartedAt = DateTime.makeUnsafe(0)
|
|
72
|
+
let inProgress = 0
|
|
55
73
|
|
|
56
|
-
|
|
57
|
-
yield*
|
|
74
|
+
while (true) {
|
|
75
|
+
yield* semaphore.take(1)
|
|
76
|
+
if (isFinite && iteration >= iterations) {
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const currentIteration = iteration
|
|
81
|
+
|
|
82
|
+
if (inProgress > 0) {
|
|
83
|
+
// add delay to try keep task list in sync
|
|
84
|
+
const nextEarliestStart = lastStartedAt.pipe(
|
|
85
|
+
DateTime.add({ seconds: 30 }),
|
|
86
|
+
)
|
|
87
|
+
const diff = DateTime.distance(yield* DateTime.now, nextEarliestStart)
|
|
88
|
+
if (diff > 0) {
|
|
89
|
+
yield* Effect.sleep(diff)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lastStartedAt = yield* DateTime.now
|
|
94
|
+
inProgress++
|
|
95
|
+
|
|
96
|
+
yield* run.pipe(
|
|
97
|
+
Effect.catchTag("NoMoreWork", (e) => {
|
|
98
|
+
if (isFinite) {
|
|
99
|
+
// If we have a finite number of iterations, we exit when no more
|
|
100
|
+
// work is found
|
|
101
|
+
iterations = currentIteration
|
|
102
|
+
return Effect.fail(e)
|
|
103
|
+
}
|
|
104
|
+
return Effect.log(
|
|
105
|
+
"No more work to process, waiting 30 seconds...",
|
|
106
|
+
).pipe(Effect.andThen(Effect.sleep("30 seconds")))
|
|
107
|
+
}),
|
|
108
|
+
Effect.catchCause(Effect.logWarning),
|
|
109
|
+
Effect.annotateLogs({
|
|
110
|
+
iteration: currentIteration,
|
|
111
|
+
}),
|
|
112
|
+
Effect.ensuring(
|
|
113
|
+
Effect.suspend(() => {
|
|
114
|
+
inProgress--
|
|
115
|
+
return semaphore.release(1)
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
Effect.forkChild,
|
|
119
|
+
)
|
|
120
|
+
iteration++
|
|
58
121
|
}
|
|
59
122
|
}),
|
|
60
123
|
),
|
|
@@ -64,6 +127,10 @@ const root = Command.make("lalph", { iterations }).pipe(
|
|
|
64
127
|
Command.run(root, {
|
|
65
128
|
version: "0.1.0",
|
|
66
129
|
}).pipe(
|
|
67
|
-
Effect.provide(
|
|
130
|
+
Effect.provide(
|
|
131
|
+
Layer.mergeAll(Settings.layer, Linear.layer).pipe(
|
|
132
|
+
Layer.provideMerge(NodeServices.layer),
|
|
133
|
+
),
|
|
134
|
+
),
|
|
68
135
|
NodeRuntime.runMain,
|
|
69
136
|
)
|