lalph 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -7188,7 +7188,7 @@ const getOption = /* @__PURE__ */ dual(2, (self, service) => {
7188
7188
  * @since 4.0.0
7189
7189
  * @category Utils
7190
7190
  */
7191
- const merge$5 = /* @__PURE__ */ dual(2, (self, that) => {
7191
+ const merge$6 = /* @__PURE__ */ dual(2, (self, that) => {
7192
7192
  if (self.mapUnsafe.size === 0) return that;
7193
7193
  if (that.mapUnsafe.size === 0) return self;
7194
7194
  const map = new Map(self.mapUnsafe);
@@ -9159,7 +9159,7 @@ const servicesWith$1 = (f) => withFiber$1((fiber) => f(fiber.services));
9159
9159
  /** @internal */
9160
9160
  const provideServices$1 = /* @__PURE__ */ dual(2, (self, services) => {
9161
9161
  if (effectIsExit(self)) return self;
9162
- return updateServices$1(self, merge$5(services));
9162
+ return updateServices$1(self, merge$6(services));
9163
9163
  });
9164
9164
  /** @internal */
9165
9165
  const provideService$1 = function() {
@@ -11960,6 +11960,38 @@ const mergeAllEffect = (layers, memoMap, scope) => {
11960
11960
  * @category zipping
11961
11961
  */
11962
11962
  const mergeAll = (...layers) => fromBuild((memoMap, scope) => mergeAllEffect(layers, memoMap, scope));
11963
+ /**
11964
+ * Merges this layer with the specified layer concurrently, producing a new layer with combined input and output types.
11965
+ *
11966
+ * This is a binary version of `mergeAll` that merges exactly two layers or one layer with an array of layers.
11967
+ * The layers are built concurrently and their outputs are combined.
11968
+ *
11969
+ * @example
11970
+ * ```ts
11971
+ * import { Effect, Layer, ServiceMap } from "effect"
11972
+ *
11973
+ * class Database extends ServiceMap.Service<Database, {
11974
+ * readonly query: (sql: string) => Effect.Effect<string>
11975
+ * }>()("Database") {}
11976
+ *
11977
+ * class Logger extends ServiceMap.Service<Logger, {
11978
+ * readonly log: (msg: string) => Effect.Effect<void>
11979
+ * }>()("Logger") {}
11980
+ *
11981
+ * const dbLayer = Layer.succeed(Database)({
11982
+ * query: (sql: string) => Effect.succeed("result")
11983
+ * })
11984
+ * const loggerLayer = Layer.succeed(Logger)({
11985
+ * log: (msg: string) => Effect.sync(() => console.log(msg))
11986
+ * })
11987
+ *
11988
+ * const mergedLayer = Layer.merge(dbLayer, loggerLayer)
11989
+ * ```
11990
+ *
11991
+ * @since 2.0.0
11992
+ * @category zipping
11993
+ */
11994
+ const merge$5 = /* @__PURE__ */ dual(2, (self, that) => mergeAll(self, ...Array.isArray(that) ? that : [that]));
11963
11995
  const provideWith = (self, that, f) => fromBuild((memoMap, scope) => flatMap$4(Array.isArray(that) ? mergeAllEffect(that, memoMap, scope) : that.build(memoMap, scope), (context) => self.build(memoMap, scope).pipe(provideServices$1(context), map$11((merged) => f(merged, context)))));
11964
11996
  /**
11965
11997
  * Feeds the output services of this builder into the input of the specified
@@ -12102,7 +12134,7 @@ const provide$3 = /* @__PURE__ */ dual(2, (self, that) => provideWith(self, that
12102
12134
  * @since 2.0.0
12103
12135
  * @category utils
12104
12136
  */
12105
- const provideMerge = /* @__PURE__ */ dual(2, (self, that) => provideWith(self, that, (self, that) => merge$5(that, self)));
12137
+ const provideMerge = /* @__PURE__ */ dual(2, (self, that) => provideWith(self, that, (self, that) => merge$6(that, self)));
12106
12138
  /**
12107
12139
  * Constructs a layer dynamically based on the output of this layer.
12108
12140
  *
@@ -50856,7 +50888,7 @@ const TypeId$27 = "~effect/Cache";
50856
50888
  */
50857
50889
  const makeWith$1 = (options) => servicesWith$1((services) => {
50858
50890
  const self = Object.create(Proto$12);
50859
- self.lookup = (key) => updateServices$1(options.lookup(key), (input) => merge$5(services, input));
50891
+ self.lookup = (key) => updateServices$1(options.lookup(key), (input) => merge$6(services, input));
50860
50892
  self.map = make$45();
50861
50893
  self.capacity = options.capacity;
50862
50894
  self.timeToLive = options.timeToLive ? (exit, key) => fromDurationInputUnsafe(options.timeToLive(exit, key)) : defaultTimeToLive;
@@ -58544,7 +58576,7 @@ const SpanNameGenerator$1 = /* @__PURE__ */ Reference("effect/http/HttpClient/Sp
58544
58576
  /**
58545
58577
  * @since 4.0.0
58546
58578
  */
58547
- const layerMergedServices = (effect) => effect$1(HttpClient)(servicesWith((services) => map$8(effect, (client) => transformResponse(client, updateServices((input) => merge$5(services, input))))));
58579
+ const layerMergedServices = (effect) => effect$1(HttpClient)(servicesWith((services) => map$8(effect, (client) => transformResponse(client, updateServices((input) => merge$6(services, input))))));
58548
58580
  const responseRegistry = /* @__PURE__ */ (() => {
58549
58581
  if ("FinalizationRegistry" in globalThis && globalThis.FinalizationRegistry) {
58550
58582
  const registry = /* @__PURE__ */ new FinalizationRegistry((controller) => {
@@ -59849,7 +59881,7 @@ const fromWebSocket = (acquire, options) => withFiber((fiber) => {
59849
59881
  latch.openUnsafe();
59850
59882
  if (opts?.onOpen) yield* opts.onOpen;
59851
59883
  return yield* join(fiberSet).pipe(catchFilter(SocketCloseError.filterClean((_) => !closeCodeIsError(_)), (_) => void_$1));
59852
- })).pipe(updateServices((input) => merge$5(acquireContext, input)), ensuring$2(sync(() => {
59884
+ })).pipe(updateServices((input) => merge$6(acquireContext, input)), ensuring$2(sync(() => {
59853
59885
  latch.closeUnsafe();
59854
59886
  currentWS = void 0;
59855
59887
  })));
@@ -66689,23 +66721,14 @@ const PlatformServices = layer$2;
66689
66721
  //#endregion
66690
66722
  //#region src/domain/Project.ts
66691
66723
  const ProjectId = String$1.pipe(brand("lalph/ProjectId"));
66692
- var Project$1 = class Project$1 extends Class("lalph/Project")({
66724
+ var Project$1 = class extends Class("lalph/Project")({
66693
66725
  id: ProjectId,
66694
66726
  enabled: Boolean$2,
66695
66727
  targetBranch: Option(String$1),
66696
66728
  concurrency: Int.check(isGreaterThanOrEqualTo(1)),
66697
66729
  gitFlow: Literals(["pr", "commit"]),
66698
66730
  reviewAgent: Boolean$2
66699
- }) {
66700
- static defaultProject = new Project$1({
66701
- id: ProjectId.makeUnsafe("default"),
66702
- enabled: true,
66703
- targetBranch: none$3(),
66704
- concurrency: 1,
66705
- gitFlow: "pr",
66706
- reviewAgent: true
66707
- });
66708
- };
66731
+ }) {};
66709
66732
 
66710
66733
  //#endregion
66711
66734
  //#region src/Kvs.ts
@@ -151185,9 +151208,9 @@ const agentTimeout = fnUntraced(function* (options) {
151185
151208
  //#region src/Projects.ts
151186
151209
  const layerProjectIdPrompt = effect$1(CurrentProjectId, gen(function* () {
151187
151210
  return (yield* selectProject).id;
151188
- })).pipe(provide$3(Settings.layer));
151189
- const allProjects = new Setting("projects", NonEmptyArray(Project$1));
151190
- const getAllProjects = Settings.get(allProjects).pipe(map$8(getOrElse$1(() => [Project$1.defaultProject])));
151211
+ })).pipe(provide$3(Settings.layer), provide$3(CurrentIssueSource.layer));
151212
+ const allProjects = new Setting("projects", Array$1(Project$1));
151213
+ const getAllProjects = Settings.get(allProjects).pipe(map$8(getOrElse$1(() => [])));
151191
151214
  const projectById = fnUntraced(function* (projectId) {
151192
151215
  const projects = yield* getAllProjects;
151193
151216
  return findFirst$3(projects, (p) => p.id === projectId);
@@ -151195,7 +151218,7 @@ const projectById = fnUntraced(function* (projectId) {
151195
151218
  const allProjectsAtom = (function() {
151196
151219
  const read = Settings.runtime.atom(fnUntraced(function* () {
151197
151220
  const projects = yield* (yield* Settings).get(allProjects);
151198
- return getOrElse$1(projects, () => [Project$1.defaultProject]);
151221
+ return getOrElse$1(projects, () => []);
151199
151222
  }));
151200
151223
  const set = Settings.runtime.fn()(fnUntraced(function* (value, get) {
151201
151224
  yield* (yield* Settings).set(allProjects, some$2(value));
@@ -151219,7 +151242,6 @@ const projectAtom = family((projectId) => {
151219
151242
  onNone: () => filter$6(projects, (p) => p.id !== projectId),
151220
151243
  onSome: (project) => map$12(projects, (p) => p.id === projectId ? project : p)
151221
151244
  });
151222
- if (!isArrayNonEmpty(updatedProjects)) return;
151223
151245
  get.set(allProjectsAtom, updatedProjects);
151224
151246
  }));
151225
151247
  return writable((get) => {
@@ -151231,9 +151253,11 @@ const projectAtom = family((projectId) => {
151231
151253
  });
151232
151254
  const selectProject = gen(function* () {
151233
151255
  const projects = yield* getAllProjects;
151234
- if (projects.length === 1) {
151235
- yield* log$1(`Using project: ${projects[0].id}`);
151236
- return projects[0];
151256
+ if (projects.length === 0) return yield* welcomeWizard;
151257
+ else if (projects.length === 1) {
151258
+ const project = projects[0];
151259
+ yield* log$1(`Using project: ${project.id}`);
151260
+ return project;
151237
151261
  }
151238
151262
  return yield* autoComplete({
151239
151263
  message: "Select a project:",
@@ -151243,6 +151267,62 @@ const selectProject = gen(function* () {
151243
151267
  }))
151244
151268
  });
151245
151269
  });
151270
+ const welcomeWizard = gen(function* () {
151271
+ const welcome = [
151272
+ " .--.",
151273
+ " |^()^| lalph",
151274
+ " '--'",
151275
+ "",
151276
+ "Welcome! Let's add your first project.",
151277
+ "Projects let you configure how lalph runs tasks.",
151278
+ ""
151279
+ ].join("\n");
151280
+ console.log(welcome);
151281
+ return yield* addOrUpdateProject();
151282
+ });
151283
+ const addOrUpdateProject = fnUntraced(function* (existing) {
151284
+ const projects = yield* getAllProjects;
151285
+ const id = existing ? existing.id : yield* text$2({
151286
+ message: "Project name",
151287
+ validate(input) {
151288
+ input = input.trim();
151289
+ if (input.length === 0) return fail$4("Project name cannot be empty");
151290
+ else if (projects.some((p) => p.id === input)) return fail$4("Project already exists");
151291
+ return succeed$1(input);
151292
+ }
151293
+ });
151294
+ const concurrency = yield* integer$2({
151295
+ message: "Concurrency (number of tasks to run in parallel)",
151296
+ min: 1
151297
+ });
151298
+ const targetBranch = pipe(yield* text$2({ message: "Target branch (leave empty to use HEAD)" }), trim, liftPredicate(isNonEmpty));
151299
+ const gitFlow = yield* select({
151300
+ message: "Git flow",
151301
+ choices: [{
151302
+ title: "Pull Request",
151303
+ description: "Create a pull request for each task",
151304
+ value: "pr"
151305
+ }, {
151306
+ title: "Commit",
151307
+ description: "Tasks are committed directly to the target branch",
151308
+ value: "commit"
151309
+ }]
151310
+ });
151311
+ const reviewAgent = yield* toggle({ message: "Enable review agent?" });
151312
+ const project = new Project$1({
151313
+ id: ProjectId.makeUnsafe(id),
151314
+ enabled: existing ? existing.enabled : true,
151315
+ concurrency,
151316
+ targetBranch,
151317
+ gitFlow,
151318
+ reviewAgent
151319
+ });
151320
+ yield* Settings.set(allProjects, some$2(existing ? projects.map((p) => p.id === project.id ? project : p) : [...projects, project]));
151321
+ const source = yield* IssueSource;
151322
+ yield* source.reset.pipe(provideService(CurrentProjectId, project.id));
151323
+ yield* source.settings(project.id);
151324
+ return project;
151325
+ });
151246
151326
 
151247
151327
  //#endregion
151248
151328
  //#region src/commands/root.ts
@@ -151403,7 +151483,13 @@ const commandRoot = make$35("lalph", {
151403
151483
  }).pipe(withHandler(fnUntraced(function* ({ iterations, maxIterationMinutes, stallMinutes, specsDirectory }) {
151404
151484
  const commandPrefix = yield* getCommandPrefix;
151405
151485
  yield* getOrSelectCliAgent;
151406
- const projects = (yield* getAllProjects).filter((p) => p.enabled);
151486
+ let allProjects = yield* getAllProjects;
151487
+ if (allProjects.length === 0) {
151488
+ yield* welcomeWizard;
151489
+ allProjects = yield* getAllProjects;
151490
+ }
151491
+ const projects = allProjects.filter((p) => p.enabled);
151492
+ if (projects.length === 0) return yield* log$1("No enabled projects found. Run 'lalph projects toggle' to enable one.");
151407
151493
  yield* forEach$3(projects, (project) => runProject({
151408
151494
  iterations,
151409
151495
  project,
@@ -151437,7 +151523,7 @@ const commandPlan = make$35("plan", { dangerous }).pipe(withDescription("Iterate
151437
151523
  commandPrefix,
151438
151524
  dangerous
151439
151525
  }).pipe(provideService(CurrentProjectId, project.id));
151440
- }, provide$1(Settings.layer))));
151526
+ }, provide$1([Settings.layer, CurrentIssueSource.layer]))));
151441
151527
  const plan = fnUntraced(function* (options) {
151442
151528
  const fs = yield* FileSystem;
151443
151529
  const pathService = yield* Path$1;
@@ -151523,7 +151609,7 @@ const handler$1 = flow(withHandler(fnUntraced(function* () {
151523
151609
  }));
151524
151610
  console.log(`Created issue with ID: ${created.id}`);
151525
151611
  console.log(`URL: ${created.url}`);
151526
- }, scoped$1)), provide(mergeAll(CurrentIssueSource.layer, layerProjectIdPrompt)));
151612
+ }, scoped$1)), provide(merge$5(layerProjectIdPrompt, CurrentIssueSource.layer)));
151527
151613
  const commandIssue = make$35("issue").pipe(withDescription("Create a new issue in the selected issue source"), handler$1);
151528
151614
  const commandIssueAlias = make$35("i").pipe(withDescription("Alias for 'issue' command"), handler$1);
151529
151615
 
@@ -151548,7 +151634,7 @@ const commandSource = make$35("source").pipe(withDescription("Select the issue s
151548
151634
 
151549
151635
  //#endregion
151550
151636
  //#region package.json
151551
- var version = "0.2.1";
151637
+ var version = "0.2.3";
151552
151638
 
151553
151639
  //#endregion
151554
151640
  //#region src/commands/projects/ls.ts
@@ -151558,6 +151644,10 @@ const commandProjectsLs = make$35("ls").pipe(withDescription("List all configure
151558
151644
  console.log("Issue source:", meta.name);
151559
151645
  console.log("");
151560
151646
  const projects = yield* getAllProjects;
151647
+ if (projects.length === 0) {
151648
+ console.log("No projects configured yet. Run 'lalph projects add' to get started.");
151649
+ return;
151650
+ }
151561
151651
  for (const project of projects) {
151562
151652
  console.log(`Project: ${project.id}`);
151563
151653
  console.log(` Enabled: ${project.enabled ? "Yes" : "No"}`);
@@ -151572,93 +151662,30 @@ const commandProjectsLs = make$35("ls").pipe(withDescription("List all configure
151572
151662
 
151573
151663
  //#endregion
151574
151664
  //#region src/commands/projects/add.ts
151575
- const commandProjectsAdd = make$35("add").pipe(withDescription("Add a new project"), withHandler(fnUntraced(function* () {
151576
- const projects = yield* getAllProjects;
151577
- const id = yield* text$2({
151578
- message: "Name",
151579
- validate(input) {
151580
- input = input.trim();
151581
- if (input.length === 0) return fail$4("Project name cannot be empty");
151582
- else if (projects.some((p) => p.id === input)) return fail$4(`Project already exists`);
151583
- return succeed$1(input);
151584
- }
151585
- });
151586
- const concurrency = yield* integer$2({
151587
- message: "Concurrency",
151588
- min: 1
151589
- });
151590
- const targetBranch = pipe(yield* text$2({ message: "Target branch (leave empty to use HEAD)" }), trim, liftPredicate(isNonEmpty));
151591
- const gitFlow = yield* select({
151592
- message: "Git flow",
151593
- choices: [{
151594
- title: "Pull Request",
151595
- value: "pr"
151596
- }, {
151597
- title: "Commit",
151598
- value: "commit"
151599
- }]
151600
- });
151601
- const reviewAgent = yield* toggle({ message: "Enable review agent?" });
151602
- const project = new Project$1({
151603
- id: ProjectId.makeUnsafe(id),
151604
- enabled: true,
151605
- concurrency,
151606
- targetBranch,
151607
- gitFlow,
151608
- reviewAgent
151609
- });
151610
- yield* Settings.set(allProjects, some$2([...projects, project]));
151611
- yield* (yield* IssueSource).settings(project.id);
151612
- })), provide(Settings.layer), provide(CurrentIssueSource.layer));
151665
+ const commandProjectsAdd = make$35("add").pipe(withDescription("Add a new project"), withHandler(() => addOrUpdateProject()), provide(Settings.layer), provide(CurrentIssueSource.layer));
151613
151666
 
151614
151667
  //#endregion
151615
151668
  //#region src/commands/projects/rm.ts
151616
151669
  const commandProjectsRm = make$35("rm").pipe(withDescription("Remove a project"), withHandler(fnUntraced(function* () {
151617
151670
  const projects = yield* getAllProjects;
151671
+ if (projects.length === 0) return yield* log$1("There are no projects to remove.");
151618
151672
  const project = yield* selectProject;
151619
151673
  const newProjects = projects.filter((p) => p.id !== project.id);
151620
- if (!isArrayNonEmpty(newProjects)) return yield* log$1("You cannot remove the last remaining project.");
151621
151674
  yield* Settings.set(allProjects, some$2(newProjects));
151622
- })), provide(Settings.layer));
151675
+ })), provide(Settings.layer), provide(CurrentIssueSource.layer));
151623
151676
 
151624
151677
  //#endregion
151625
151678
  //#region src/commands/projects/edit.ts
151626
151679
  const commandProjectsEdit = make$35("edit").pipe(withDescription("Modify a project"), withHandler(fnUntraced(function* () {
151627
- const projects = yield* getAllProjects;
151628
- const project = yield* selectProject;
151629
- const concurrency = yield* integer$2({
151630
- message: "Concurrency",
151631
- min: 1
151632
- });
151633
- const targetBranch = pipe(yield* text$2({ message: "Target branch (leave empty to use HEAD)" }), trim, liftPredicate(isNonEmpty));
151634
- const gitFlow = yield* select({
151635
- message: "Git flow",
151636
- choices: [{
151637
- title: "Pull Request",
151638
- value: "pr"
151639
- }, {
151640
- title: "Commit",
151641
- value: "commit"
151642
- }]
151643
- });
151644
- const reviewAgent = yield* toggle({ message: "Enable review agent?" });
151645
- const nextProject = new Project$1({
151646
- ...project,
151647
- concurrency,
151648
- targetBranch,
151649
- gitFlow,
151650
- reviewAgent
151651
- });
151652
- yield* Settings.set(allProjects, some$2(map$12(projects, (p) => p.id === nextProject.id ? nextProject : p)));
151653
- const source = yield* IssueSource;
151654
- yield* source.reset.pipe(provideService(CurrentProjectId, nextProject.id));
151655
- yield* source.settings(project.id);
151680
+ if ((yield* getAllProjects).length === 0) return yield* log$1("No projects available to edit.");
151681
+ yield* addOrUpdateProject(yield* selectProject);
151656
151682
  })), provide(Settings.layer), provide(CurrentIssueSource.layer));
151657
151683
 
151658
151684
  //#endregion
151659
151685
  //#region src/commands/projects/toggle.ts
151660
151686
  const commandProjectsToggle = make$35("toggle").pipe(withDescription("Enable or disable projects"), withHandler(fnUntraced(function* () {
151661
151687
  const projects = yield* getAllProjects;
151688
+ if (projects.length === 0) return yield* log$1("No projects available to toggle.");
151662
151689
  const enabled = yield* multiSelect({
151663
151690
  message: "Select projects to enable",
151664
151691
  choices: projects.map((project) => ({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lalph",
3
3
  "type": "module",
4
- "version": "0.2.1",
4
+ "version": "0.2.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
package/src/Projects.ts CHANGED
@@ -4,14 +4,17 @@ import {
4
4
  Effect,
5
5
  Layer,
6
6
  Option,
7
+ pipe,
7
8
  PlatformError,
8
9
  Schema,
10
+ String,
9
11
  } from "effect"
10
- import { Project, type ProjectId } from "./domain/Project.ts"
12
+ import { Project, ProjectId } from "./domain/Project.ts"
11
13
  import { AsyncResult, Atom } from "effect/unstable/reactivity"
12
14
  import { CurrentProjectId, Setting, Settings } from "./Settings.ts"
13
15
  import { Prompt } from "effect/unstable/cli"
14
- import type { NonEmptyReadonlyArray } from "effect/Array"
16
+ import { IssueSource } from "./IssueSource.ts"
17
+ import { CurrentIssueSource } from "./IssueSources.ts"
15
18
 
16
19
  export const layerProjectIdPrompt = Layer.effect(
17
20
  CurrentProjectId,
@@ -19,19 +22,12 @@ export const layerProjectIdPrompt = Layer.effect(
19
22
  const project = yield* selectProject
20
23
  return project.id
21
24
  }),
22
- ).pipe(Layer.provide(Settings.layer))
25
+ ).pipe(Layer.provide(Settings.layer), Layer.provide(CurrentIssueSource.layer))
23
26
 
24
- export const allProjects = new Setting(
25
- "projects",
26
- Schema.NonEmptyArray(Project),
27
- )
27
+ export const allProjects = new Setting("projects", Schema.Array(Project))
28
28
 
29
29
  export const getAllProjects = Settings.get(allProjects).pipe(
30
- Effect.map(
31
- Option.getOrElse(
32
- (): NonEmptyReadonlyArray<Project> => [Project.defaultProject],
33
- ),
34
- ),
30
+ Effect.map(Option.getOrElse((): ReadonlyArray<Project> => [])),
35
31
  )
36
32
 
37
33
  export const projectById = Effect.fnUntraced(function* (projectId: ProjectId) {
@@ -44,13 +40,10 @@ export const allProjectsAtom = (function () {
44
40
  Effect.fnUntraced(function* () {
45
41
  const settings = yield* Settings
46
42
  const projects = yield* settings.get(allProjects)
47
- return Option.getOrElse(
48
- projects,
49
- (): Array.NonEmptyReadonlyArray<Project> => [Project.defaultProject],
50
- )
43
+ return Option.getOrElse(projects, (): ReadonlyArray<Project> => [])
51
44
  }),
52
45
  )
53
- const set = Settings.runtime.fn<Array.NonEmptyReadonlyArray<Project>>()(
46
+ const set = Settings.runtime.fn<ReadonlyArray<Project>>()(
54
47
  Effect.fnUntraced(function* (value, get) {
55
48
  const settings = yield* Settings
56
49
  yield* settings.set(allProjects, Option.some(value))
@@ -62,7 +55,7 @@ export const allProjectsAtom = (function () {
62
55
  get.mount(set)
63
56
  return get(read)
64
57
  },
65
- (ctx, value: Array.NonEmptyReadonlyArray<Project>) => {
58
+ (ctx, value: ReadonlyArray<Project>) => {
66
59
  ctx.set(set, value)
67
60
  },
68
61
  (r) => r(read),
@@ -93,7 +86,6 @@ export const projectAtom = Atom.family(
93
86
  onSome: (project) =>
94
87
  Array.map(projects, (p) => (p.id === projectId ? project : p)),
95
88
  })
96
- if (!Array.isArrayNonEmpty(updatedProjects)) return
97
89
  get.set(allProjectsAtom, updatedProjects)
98
90
  }),
99
91
  )
@@ -120,15 +112,105 @@ export class ProjectNotFound extends Data.TaggedError("ProjectNotFound")<{
120
112
 
121
113
  export const selectProject = Effect.gen(function* () {
122
114
  const projects = yield* getAllProjects
123
- if (projects.length === 1) {
124
- yield* Effect.log(`Using project: ${projects[0].id}`)
125
- return projects[0]
115
+ if (projects.length === 0) {
116
+ return yield* welcomeWizard
117
+ } else if (projects.length === 1) {
118
+ const project = projects[0]!
119
+ yield* Effect.log(`Using project: ${project.id}`)
120
+ return project
126
121
  }
127
- return yield* Prompt.autoComplete({
122
+ const selection = yield* Prompt.autoComplete({
128
123
  message: "Select a project:",
129
124
  choices: projects.map((p) => ({
130
125
  title: p.id,
131
126
  value: p,
132
127
  })),
133
128
  })
129
+ return selection!
130
+ })
131
+
132
+ export const welcomeWizard = Effect.gen(function* () {
133
+ const welcome = [
134
+ " .--.",
135
+ " |^()^| lalph",
136
+ " '--'",
137
+ "",
138
+ "Welcome! Let's add your first project.",
139
+ "Projects let you configure how lalph runs tasks.",
140
+ "",
141
+ ].join("\n")
142
+ console.log(welcome)
143
+ return yield* addOrUpdateProject()
144
+ })
145
+
146
+ export const addOrUpdateProject = Effect.fnUntraced(function* (
147
+ existing?: Project,
148
+ ) {
149
+ const projects = yield* getAllProjects
150
+ const id = existing
151
+ ? existing.id
152
+ : yield* Prompt.text({
153
+ message: "Project name",
154
+ validate(input) {
155
+ input = input.trim()
156
+ if (input.length === 0) {
157
+ return Effect.fail("Project name cannot be empty")
158
+ } else if (projects.some((p) => p.id === input)) {
159
+ return Effect.fail("Project already exists")
160
+ }
161
+ return Effect.succeed(input)
162
+ },
163
+ })
164
+ const concurrency = yield* Prompt.integer({
165
+ message: "Concurrency (number of tasks to run in parallel)",
166
+ min: 1,
167
+ })
168
+ const targetBranch = pipe(
169
+ yield* Prompt.text({
170
+ message: "Target branch (leave empty to use HEAD)",
171
+ }),
172
+ String.trim,
173
+ Option.liftPredicate(String.isNonEmpty),
174
+ )
175
+ const gitFlow = yield* Prompt.select({
176
+ message: "Git flow",
177
+ choices: [
178
+ {
179
+ title: "Pull Request",
180
+ description: "Create a pull request for each task",
181
+ value: "pr",
182
+ },
183
+ {
184
+ title: "Commit",
185
+ description: "Tasks are committed directly to the target branch",
186
+ value: "commit",
187
+ },
188
+ ] as const,
189
+ })
190
+ const reviewAgent = yield* Prompt.toggle({
191
+ message: "Enable review agent?",
192
+ })
193
+
194
+ const project = new Project({
195
+ id: ProjectId.makeUnsafe(id),
196
+ enabled: existing ? existing.enabled : true,
197
+ concurrency,
198
+ targetBranch,
199
+ gitFlow,
200
+ reviewAgent,
201
+ })
202
+ yield* Settings.set(
203
+ allProjects,
204
+ Option.some(
205
+ existing
206
+ ? projects.map((p) => (p.id === project.id ? project : p))
207
+ : [...projects, project],
208
+ ),
209
+ )
210
+
211
+ const source = yield* IssueSource
212
+ yield* source.reset.pipe(Effect.provideService(CurrentProjectId, project.id))
213
+ yield* source.settings(project.id)
214
+
215
+ return project
134
216
  })
@@ -93,9 +93,7 @@ const handler = flow(
93
93
  console.log(`URL: ${created.url}`)
94
94
  }, Effect.scoped),
95
95
  ),
96
- Command.provide(
97
- Layer.mergeAll(CurrentIssueSource.layer, layerProjectIdPrompt),
98
- ),
96
+ Command.provide(Layer.merge(layerProjectIdPrompt, CurrentIssueSource.layer)),
99
97
  )
100
98
 
101
99
  export const commandIssue = Command.make("issue").pipe(
@@ -20,17 +20,20 @@ const dangerous = Flag.boolean("dangerous").pipe(
20
20
  export const commandPlan = Command.make("plan", { dangerous }).pipe(
21
21
  Command.withDescription("Iterate on an issue plan and create PRD tasks"),
22
22
  Command.withHandler(
23
- Effect.fnUntraced(function* ({ dangerous }) {
24
- const project = yield* selectProject
25
- const { specsDirectory } = yield* commandRoot
26
- const commandPrefix = yield* getCommandPrefix
27
- yield* plan({
28
- specsDirectory,
29
- targetBranch: project.targetBranch,
30
- commandPrefix,
31
- dangerous,
32
- }).pipe(Effect.provideService(CurrentProjectId, project.id))
33
- }, Effect.provide(Settings.layer)),
23
+ Effect.fnUntraced(
24
+ function* ({ dangerous }) {
25
+ const project = yield* selectProject
26
+ const { specsDirectory } = yield* commandRoot
27
+ const commandPrefix = yield* getCommandPrefix
28
+ yield* plan({
29
+ specsDirectory,
30
+ targetBranch: project.targetBranch,
31
+ commandPrefix,
32
+ dangerous,
33
+ }).pipe(Effect.provideService(CurrentProjectId, project.id))
34
+ },
35
+ Effect.provide([Settings.layer, CurrentIssueSource.layer]),
36
+ ),
34
37
  ),
35
38
  )
36
39
  const plan = Effect.fnUntraced(
@@ -1,64 +1,11 @@
1
- import { Effect, Option, pipe, String } from "effect"
2
- import { Command, Prompt } from "effect/unstable/cli"
3
- import { allProjects, getAllProjects } from "../../Projects.ts"
4
- import { Settings } from "../../Settings.ts"
5
- import { Project, ProjectId } from "../../domain/Project.ts"
6
- import { IssueSource } from "../../IssueSource.ts"
1
+ import { Command } from "effect/unstable/cli"
2
+ import { addOrUpdateProject } from "../../Projects.ts"
7
3
  import { CurrentIssueSource } from "../../IssueSources.ts"
4
+ import { Settings } from "../../Settings.ts"
8
5
 
9
6
  export const commandProjectsAdd = Command.make("add").pipe(
10
7
  Command.withDescription("Add a new project"),
11
- Command.withHandler(
12
- Effect.fnUntraced(function* () {
13
- const projects = yield* getAllProjects
14
- const id = yield* Prompt.text({
15
- message: "Name",
16
- validate(input) {
17
- input = input.trim()
18
- if (input.length === 0) {
19
- return Effect.fail("Project name cannot be empty")
20
- } else if (projects.some((p) => p.id === input)) {
21
- return Effect.fail(`Project already exists`)
22
- }
23
- return Effect.succeed(input)
24
- },
25
- })
26
- const concurrency = yield* Prompt.integer({
27
- message: "Concurrency",
28
- min: 1,
29
- })
30
- const targetBranch = pipe(
31
- yield* Prompt.text({
32
- message: "Target branch (leave empty to use HEAD)",
33
- }),
34
- String.trim,
35
- Option.liftPredicate(String.isNonEmpty),
36
- )
37
- const gitFlow = yield* Prompt.select({
38
- message: "Git flow",
39
- choices: [
40
- { title: "Pull Request", value: "pr" },
41
- { title: "Commit", value: "commit" },
42
- ] as const,
43
- })
44
- const reviewAgent = yield* Prompt.toggle({
45
- message: "Enable review agent?",
46
- })
47
-
48
- const project = new Project({
49
- id: ProjectId.makeUnsafe(id),
50
- enabled: true,
51
- concurrency,
52
- targetBranch,
53
- gitFlow,
54
- reviewAgent,
55
- })
56
- yield* Settings.set(allProjects, Option.some([...projects, project]))
57
-
58
- const source = yield* IssueSource
59
- yield* source.settings(project.id)
60
- }),
61
- ),
8
+ Command.withHandler(() => addOrUpdateProject()),
62
9
  Command.provide(Settings.layer),
63
10
  Command.provide(CurrentIssueSource.layer),
64
11
  )
@@ -1,9 +1,11 @@
1
- import { Array, Effect, Option, pipe, String } from "effect"
2
- import { Command, Prompt } from "effect/unstable/cli"
3
- import { allProjects, getAllProjects, selectProject } from "../../Projects.ts"
4
- import { CurrentProjectId, Settings } from "../../Settings.ts"
5
- import { Project } from "../../domain/Project.ts"
6
- import { IssueSource } from "../../IssueSource.ts"
1
+ import { Effect } from "effect"
2
+ import { Command } from "effect/unstable/cli"
3
+ import {
4
+ addOrUpdateProject,
5
+ getAllProjects,
6
+ selectProject,
7
+ } from "../../Projects.ts"
8
+ import { Settings } from "../../Settings.ts"
7
9
  import { CurrentIssueSource } from "../../IssueSources.ts"
8
10
 
9
11
  export const commandProjectsEdit = Command.make("edit").pipe(
@@ -11,50 +13,11 @@ export const commandProjectsEdit = Command.make("edit").pipe(
11
13
  Command.withHandler(
12
14
  Effect.fnUntraced(function* () {
13
15
  const projects = yield* getAllProjects
16
+ if (projects.length === 0) {
17
+ return yield* Effect.log("No projects available to edit.")
18
+ }
14
19
  const project = yield* selectProject
15
- const concurrency = yield* Prompt.integer({
16
- message: "Concurrency",
17
- min: 1,
18
- })
19
- const targetBranch = pipe(
20
- yield* Prompt.text({
21
- message: "Target branch (leave empty to use HEAD)",
22
- }),
23
- String.trim,
24
- Option.liftPredicate(String.isNonEmpty),
25
- )
26
- const gitFlow = yield* Prompt.select({
27
- message: "Git flow",
28
- choices: [
29
- { title: "Pull Request", value: "pr" },
30
- { title: "Commit", value: "commit" },
31
- ] as const,
32
- })
33
- const reviewAgent = yield* Prompt.toggle({
34
- message: "Enable review agent?",
35
- })
36
-
37
- const nextProject = new Project({
38
- ...project,
39
- concurrency,
40
- targetBranch,
41
- gitFlow,
42
- reviewAgent,
43
- })
44
- yield* Settings.set(
45
- allProjects,
46
- Option.some(
47
- Array.map(projects, (p) =>
48
- p.id === nextProject.id ? nextProject : p,
49
- ),
50
- ),
51
- )
52
-
53
- const source = yield* IssueSource
54
- yield* source.reset.pipe(
55
- Effect.provideService(CurrentProjectId, nextProject.id),
56
- )
57
- yield* source.settings(project.id)
20
+ yield* addOrUpdateProject(project)
58
21
  }),
59
22
  ),
60
23
  Command.provide(Settings.layer),
@@ -16,6 +16,12 @@ export const commandProjectsLs = Command.make("ls").pipe(
16
16
 
17
17
  const projects = yield* getAllProjects
18
18
 
19
+ if (projects.length === 0) {
20
+ console.log(
21
+ "No projects configured yet. Run 'lalph projects add' to get started.",
22
+ )
23
+ return
24
+ }
19
25
  for (const project of projects) {
20
26
  console.log(`Project: ${project.id}`)
21
27
  console.log(` Enabled: ${project.enabled ? "Yes" : "No"}`)
@@ -1,22 +1,22 @@
1
- import { Array, Effect, Option } from "effect"
1
+ import { Effect, Option } from "effect"
2
2
  import { Command } from "effect/unstable/cli"
3
3
  import { allProjects, getAllProjects, selectProject } from "../../Projects.ts"
4
4
  import { Settings } from "../../Settings.ts"
5
+ import { CurrentIssueSource } from "../../IssueSources.ts"
5
6
 
6
7
  export const commandProjectsRm = Command.make("rm").pipe(
7
8
  Command.withDescription("Remove a project"),
8
9
  Command.withHandler(
9
10
  Effect.fnUntraced(function* () {
10
11
  const projects = yield* getAllProjects
12
+ if (projects.length === 0) {
13
+ return yield* Effect.log("There are no projects to remove.")
14
+ }
11
15
  const project = yield* selectProject
12
16
  const newProjects = projects.filter((p) => p.id !== project.id)
13
- if (!Array.isArrayNonEmpty(newProjects)) {
14
- return yield* Effect.log(
15
- "You cannot remove the last remaining project.",
16
- )
17
- }
18
17
  yield* Settings.set(allProjects, Option.some(newProjects))
19
18
  }),
20
19
  ),
21
20
  Command.provide(Settings.layer),
21
+ Command.provide(CurrentIssueSource.layer),
22
22
  )
@@ -9,6 +9,9 @@ export const commandProjectsToggle = Command.make("toggle").pipe(
9
9
  Command.withHandler(
10
10
  Effect.fnUntraced(function* () {
11
11
  const projects = yield* getAllProjects
12
+ if (projects.length === 0) {
13
+ return yield* Effect.log("No projects available to toggle.")
14
+ }
12
15
  const enabled = yield* Prompt.multiSelect({
13
16
  message: "Select projects to enable",
14
17
  choices: projects.map((project) => ({
@@ -39,7 +39,7 @@ import {
39
39
  import { WorkerStatus } from "../domain/WorkerState.ts"
40
40
  import { GitFlow, GitFlowCommit, GitFlowPR } from "../GitFlow.ts"
41
41
  import { parseBranch } from "../shared/git.ts"
42
- import { getAllProjects } from "../Projects.ts"
42
+ import { getAllProjects, welcomeWizard } from "../Projects.ts"
43
43
  import type { Project } from "../domain/Project.ts"
44
44
 
45
45
  // Main iteration run logic
@@ -384,7 +384,19 @@ export const commandRoot = Command.make("lalph", {
384
384
  }) {
385
385
  const commandPrefix = yield* getCommandPrefix
386
386
  yield* getOrSelectCliAgent
387
- const projects = (yield* getAllProjects).filter((p) => p.enabled)
387
+
388
+ let allProjects = yield* getAllProjects
389
+ if (allProjects.length === 0) {
390
+ yield* welcomeWizard
391
+ allProjects = yield* getAllProjects
392
+ }
393
+
394
+ const projects = allProjects.filter((p) => p.enabled)
395
+ if (projects.length === 0) {
396
+ return yield* Effect.log(
397
+ "No enabled projects found. Run 'lalph projects toggle' to enable one.",
398
+ )
399
+ }
388
400
  yield* Effect.forEach(
389
401
  projects,
390
402
  (project) =>
@@ -1,4 +1,4 @@
1
- import { Option, Schema } from "effect"
1
+ import { Schema } from "effect"
2
2
 
3
3
  export const ProjectId = Schema.String.pipe(Schema.brand("lalph/ProjectId"))
4
4
  export type ProjectId = typeof ProjectId.Type
@@ -10,13 +10,4 @@ export class Project extends Schema.Class<Project>("lalph/Project")({
10
10
  concurrency: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
11
11
  gitFlow: Schema.Literals(["pr", "commit"]),
12
12
  reviewAgent: Schema.Boolean,
13
- }) {
14
- static defaultProject = new Project({
15
- id: ProjectId.makeUnsafe("default"),
16
- enabled: true,
17
- targetBranch: Option.none(),
18
- concurrency: 1,
19
- gitFlow: "pr",
20
- reviewAgent: true,
21
- })
22
- }
13
+ }) {}