ai-hero-cli 0.2.15 → 0.3.1

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.
Files changed (2) hide show
  1. package/bin.cjs +749 -1052
  2. package/package.json +1 -1
package/bin.cjs CHANGED
@@ -82665,6 +82665,68 @@ var isMailbox = (u) => hasProperty(u, TypeId29);
82665
82665
  var make66 = make65;
82666
82666
  var toChannel4 = toChannel3;
82667
82667
 
82668
+ // src/git-service/errors.ts
82669
+ var NotAGitRepoError = class extends Data_exports.TaggedError(
82670
+ "NotAGitRepoError"
82671
+ ) {
82672
+ };
82673
+ var FailedToFetchUpstreamError = class extends Data_exports.TaggedError(
82674
+ "FailedToFetchUpstreamError"
82675
+ ) {
82676
+ };
82677
+ var FailedToCreateBranchError = class extends Data_exports.TaggedError(
82678
+ "FailedToCreateBranchError"
82679
+ ) {
82680
+ };
82681
+ var FailedToDeleteBranchError = class extends Data_exports.TaggedError(
82682
+ "FailedToDeleteBranchError"
82683
+ ) {
82684
+ };
82685
+ var FailedToFetchOriginError = class extends Data_exports.TaggedError(
82686
+ "FailedToFetchOriginError"
82687
+ ) {
82688
+ };
82689
+ var InvalidRefError = class extends Data_exports.TaggedError(
82690
+ "InvalidRefError"
82691
+ ) {
82692
+ };
82693
+ var FailedToTrackBranchError = class extends Data_exports.TaggedError(
82694
+ "FailedToTrackBranchError"
82695
+ ) {
82696
+ };
82697
+ var FailedToResetError = class extends Data_exports.TaggedError(
82698
+ "FailedToResetError"
82699
+ ) {
82700
+ };
82701
+ var FailedToCommitError = class extends Data_exports.TaggedError(
82702
+ "FailedToCommitError"
82703
+ ) {
82704
+ };
82705
+ var CherryPickConflictError = class extends Data_exports.TaggedError(
82706
+ "CherryPickConflictError"
82707
+ ) {
82708
+ };
82709
+ var FailedToCheckoutError = class extends Data_exports.TaggedError(
82710
+ "FailedToCheckoutError"
82711
+ ) {
82712
+ };
82713
+ var FailedToPushError = class extends Data_exports.TaggedError(
82714
+ "FailedToPushError"
82715
+ ) {
82716
+ };
82717
+ var FailedToFetchError = class extends Data_exports.TaggedError(
82718
+ "FailedToFetchError"
82719
+ ) {
82720
+ };
82721
+ var MergeConflictError = class extends Data_exports.TaggedError(
82722
+ "MergeConflictError"
82723
+ ) {
82724
+ };
82725
+ var RebaseConflictError = class extends Data_exports.TaggedError(
82726
+ "RebaseConflictError"
82727
+ ) {
82728
+ };
82729
+
82668
82730
  // node_modules/.pnpm/@effect+platform@0.90.2_effect@3.17.7/node_modules/@effect/platform/dist/esm/Command.js
82669
82731
  var Command_exports2 = {};
82670
82732
  __export(Command_exports2, {
@@ -83975,11 +84037,6 @@ __export(NodeFileSystem_exports2, {
83975
84037
  layer: () => layer13
83976
84038
  });
83977
84039
  var layer13 = layer6;
83978
- var VALID_UPSTREAM_PATTERNS = [
83979
- "mattpocock",
83980
- "ai-hero-dev",
83981
- "total-typescript"
83982
- ];
83983
84040
  var GitServiceConfig = class extends Context_exports.Tag(
83984
84041
  "GitServiceConfig"
83985
84042
  )() {
@@ -83990,716 +84047,427 @@ var defaultGitServiceConfigLayer = Layer_exports.succeed(
83990
84047
  cwd: process.cwd()
83991
84048
  })
83992
84049
  );
83993
- var NotAGitRepoError = class extends Data_exports.TaggedError(
83994
- "NotAGitRepoError"
83995
- ) {
83996
- };
83997
- var FailedToFetchUpstreamError = class extends Data_exports.TaggedError(
83998
- "FailedToFetchUpstreamError"
83999
- ) {
84000
- };
84001
- var FailedToCreateBranchError = class extends Data_exports.TaggedError(
84002
- "FailedToCreateBranchError"
84003
- ) {
84004
- };
84005
- var NoUpstreamFoundError = class extends Data_exports.TaggedError(
84006
- "NoUpstreamFoundError"
84007
- ) {
84008
- };
84009
- var FailedToDeleteBranchError = class extends Data_exports.TaggedError(
84010
- "FailedToDeleteBranchError"
84011
- ) {
84012
- };
84013
- var FailedToFetchOriginError = class extends Data_exports.TaggedError(
84014
- "FailedToFetchOriginError"
84015
- ) {
84016
- };
84017
- var InvalidRefError = class extends Data_exports.TaggedError(
84018
- "InvalidRefError"
84019
- ) {
84020
- };
84021
- var FailedToTrackBranchError = class extends Data_exports.TaggedError(
84022
- "FailedToTrackBranchError"
84023
- ) {
84024
- };
84025
- var FailedToResetError = class extends Data_exports.TaggedError(
84026
- "FailedToResetError"
84027
- ) {
84028
- };
84029
- var FailedToCommitError = class extends Data_exports.TaggedError(
84030
- "FailedToCommitError"
84031
- ) {
84032
- };
84033
- var CherryPickConflictError = class extends Data_exports.TaggedError(
84034
- "CherryPickConflictError"
84035
- ) {
84036
- };
84037
- var FailedToCheckoutError = class extends Data_exports.TaggedError(
84038
- "FailedToCheckoutError"
84039
- ) {
84040
- };
84041
- var FailedToPushError = class extends Data_exports.TaggedError(
84042
- "FailedToPushError"
84043
- ) {
84044
- };
84045
- var FailedToFetchError = class extends Data_exports.TaggedError(
84046
- "FailedToFetchError"
84047
- ) {
84048
- };
84049
- var MergeConflictError = class extends Data_exports.TaggedError(
84050
- "MergeConflictError"
84051
- ) {
84052
- };
84053
- var RebaseConflictError = class extends Data_exports.TaggedError(
84054
- "RebaseConflictError"
84055
- ) {
84056
- };
84057
- var NoParentCommitError = class extends Data_exports.TaggedError(
84058
- "NoParentCommitError"
84059
- ) {
84060
- };
84061
- var GitService = class extends Effect_exports.Service()(
84062
- "GitService",
84063
- {
84064
- effect: Effect_exports.gen(function* () {
84065
- const config3 = yield* GitServiceConfig;
84066
- const fs = yield* FileSystem_exports.FileSystem;
84067
- const runCommandWithString = Effect_exports.fn(
84068
- "runCommandWithString"
84069
- )(function* (...commandArgs) {
84070
- const cwd = config3.cwd;
84071
- const command2 = Command_exports2.make(...commandArgs).pipe(
84072
- Command_exports2.workingDirectory(cwd)
84050
+ var mapExitCode = (exitCode3, makeError) => {
84051
+ if (exitCode3 !== 0) {
84052
+ return Effect_exports.fail(makeError(exitCode3));
84053
+ }
84054
+ return Effect_exports.void;
84055
+ };
84056
+ var makeGitService = Effect_exports.gen(function* () {
84057
+ const config3 = yield* GitServiceConfig;
84058
+ const fs = yield* FileSystem_exports.FileSystem;
84059
+ const runCommandWithString = Effect_exports.fn(
84060
+ "runCommandWithString"
84061
+ )(function* (...commandArgs) {
84062
+ const cwd = config3.cwd;
84063
+ const command2 = Command_exports2.make(...commandArgs).pipe(
84064
+ Command_exports2.workingDirectory(cwd)
84065
+ );
84066
+ return (yield* Command_exports2.string(command2)).trim();
84067
+ });
84068
+ const runCommandWithExitCode = Effect_exports.fn(
84069
+ "runCommandWithExitCode"
84070
+ )(function* (...commandArgs) {
84071
+ const cwd = config3.cwd;
84072
+ const command2 = Command_exports2.make(...commandArgs).pipe(
84073
+ Command_exports2.workingDirectory(cwd),
84074
+ Command_exports2.stdout("inherit"),
84075
+ Command_exports2.stderr("inherit")
84076
+ );
84077
+ return yield* Command_exports2.exitCode(command2);
84078
+ });
84079
+ const resetHard = Effect_exports.fn("resetHard")(function* (sha) {
84080
+ const exitCode3 = yield* runCommandWithExitCode(
84081
+ "git",
84082
+ "reset",
84083
+ "--hard",
84084
+ sha
84085
+ );
84086
+ yield* mapExitCode(
84087
+ exitCode3,
84088
+ (code2) => new FailedToResetError({
84089
+ sha,
84090
+ message: `Failed to reset to ${sha} (exit code: ${code2})`
84091
+ })
84092
+ );
84093
+ });
84094
+ const resetHead = Effect_exports.fn("resetHead")(function* () {
84095
+ yield* runCommandWithExitCode("git", "reset", "HEAD^");
84096
+ });
84097
+ const restoreStaged = Effect_exports.fn(
84098
+ "restoreStaged"
84099
+ )(function* () {
84100
+ yield* runCommandWithExitCode(
84101
+ "git",
84102
+ "restore",
84103
+ "--staged",
84104
+ "."
84105
+ );
84106
+ });
84107
+ return {
84108
+ fetchOrigin: Effect_exports.fn("fetchOrigin")(function* () {
84109
+ const exitCode3 = yield* runCommandWithExitCode(
84110
+ "git",
84111
+ "fetch",
84112
+ "origin"
84113
+ );
84114
+ yield* mapExitCode(
84115
+ exitCode3,
84116
+ (code2) => new FailedToFetchOriginError({
84117
+ message: `Failed to fetch from origin (exit code: ${code2})`
84118
+ })
84119
+ );
84120
+ }),
84121
+ revParse: Effect_exports.fn("revParse")(function* (ref) {
84122
+ const result = yield* runCommandWithString(
84123
+ "git",
84124
+ "rev-parse",
84125
+ ref
84126
+ ).pipe(
84127
+ Effect_exports.catchAll(
84128
+ () => Effect_exports.fail(
84129
+ new InvalidRefError({
84130
+ ref,
84131
+ message: `Failed to resolve ref: ${ref}`
84132
+ })
84133
+ )
84134
+ )
84135
+ );
84136
+ return result;
84137
+ }),
84138
+ revListCount: Effect_exports.fn("revListCount")(function* (from, to) {
84139
+ const countOutput = yield* runCommandWithString(
84140
+ "git",
84141
+ "rev-list",
84142
+ "--count",
84143
+ `${from}..${to}`
84144
+ );
84145
+ return parseInt(countOutput, 10);
84146
+ }),
84147
+ getStatusShort: Effect_exports.fn("getStatusShort")(
84148
+ function* () {
84149
+ return yield* runCommandWithString(
84150
+ "git",
84151
+ "status",
84152
+ "--short"
84073
84153
  );
84074
- return (yield* Command_exports2.string(command2)).trim();
84075
- });
84076
- const runCommandWithExitCode = Effect_exports.fn(
84077
- "runCommandWithExitCode"
84078
- )(function* (...commandArgs) {
84079
- const cwd = config3.cwd;
84080
- const command2 = Command_exports2.make(...commandArgs).pipe(
84081
- Command_exports2.workingDirectory(cwd),
84082
- Command_exports2.stdout("inherit"),
84083
- Command_exports2.stderr("inherit")
84154
+ }
84155
+ ),
84156
+ resetHard,
84157
+ resetHead,
84158
+ restoreStaged,
84159
+ applyAsUnstagedChanges: Effect_exports.fn(
84160
+ "applyAsUnstagedChanges"
84161
+ )(function* (sha) {
84162
+ yield* resetHard(sha);
84163
+ yield* resetHead();
84164
+ yield* restoreStaged();
84165
+ }),
84166
+ stageAll: Effect_exports.fn("stageAll")(function* () {
84167
+ yield* runCommandWithExitCode("git", "add", ".");
84168
+ }),
84169
+ commit: Effect_exports.fn("commit")(function* (message) {
84170
+ const exitCode3 = yield* runCommandWithExitCode(
84171
+ "git",
84172
+ "commit",
84173
+ "-m",
84174
+ message
84175
+ );
84176
+ yield* mapExitCode(
84177
+ exitCode3,
84178
+ (code2) => new FailedToCommitError({
84179
+ message: `Failed to commit (exit code: ${code2})`
84180
+ })
84181
+ );
84182
+ }),
84183
+ cherryPick: Effect_exports.fn("cherryPick")(function* (range4) {
84184
+ const exitCode3 = yield* runCommandWithExitCode(
84185
+ "git",
84186
+ "cherry-pick",
84187
+ range4
84188
+ );
84189
+ yield* mapExitCode(
84190
+ exitCode3,
84191
+ () => new CherryPickConflictError({
84192
+ range: range4,
84193
+ message: `Cherry-pick conflict on range ${range4}`
84194
+ })
84195
+ );
84196
+ }),
84197
+ cherryPickContinue: Effect_exports.fn("cherryPickContinue")(
84198
+ function* () {
84199
+ const exitCode3 = yield* runCommandWithExitCode(
84200
+ "git",
84201
+ "cherry-pick",
84202
+ "--continue"
84084
84203
  );
84085
- return yield* Command_exports2.exitCode(command2);
84086
- });
84087
- const detectUpstreamRemote = Effect_exports.fn(
84088
- "detectUpstreamRemote"
84089
- )(function* () {
84090
- const remotes = yield* runCommandWithString(
84204
+ yield* mapExitCode(
84205
+ exitCode3,
84206
+ () => new CherryPickConflictError({
84207
+ range: "continue",
84208
+ message: "Cherry-pick continue encountered conflicts"
84209
+ })
84210
+ );
84211
+ }
84212
+ ),
84213
+ cherryPickAbort: Effect_exports.fn("cherryPickAbort")(
84214
+ function* () {
84215
+ yield* runCommandWithExitCode(
84091
84216
  "git",
84092
- "remote",
84093
- "-v"
84217
+ "cherry-pick",
84218
+ "--abort"
84094
84219
  );
84095
- for (const line4 of remotes.split("\n")) {
84096
- const match18 = line4.match(/^(\S+)\s+(\S+)/);
84097
- if (match18) {
84098
- const remoteName = match18[1].trim();
84099
- const url2 = match18[2].trim();
84100
- if (VALID_UPSTREAM_PATTERNS.some(
84101
- (pattern2) => url2.includes(pattern2)
84102
- )) {
84103
- return { remoteName, url: url2 };
84104
- }
84105
- }
84106
- }
84107
- return yield* Effect_exports.fail(
84108
- new NoUpstreamFoundError({
84109
- message: `No valid upstream remote found.
84110
- Looking for repos from usernames: ${VALID_UPSTREAM_PATTERNS.join(
84111
- ", "
84112
- )}
84113
-
84114
- Add upstream remote:
84115
- git remote add upstream https://github.com/<username>/<repo>.git`
84220
+ }
84221
+ ),
84222
+ checkout: Effect_exports.fn("checkout")(function* (branch) {
84223
+ const exitCode3 = yield* runCommandWithExitCode(
84224
+ "git",
84225
+ "checkout",
84226
+ branch
84227
+ );
84228
+ yield* mapExitCode(
84229
+ exitCode3,
84230
+ (code2) => new FailedToCheckoutError({
84231
+ branch,
84232
+ message: `Failed to checkout ${branch} (exit code: ${code2})`
84233
+ })
84234
+ );
84235
+ }),
84236
+ pushForceWithLease: Effect_exports.fn("pushForceWithLease")(
84237
+ function* (remote, branch) {
84238
+ const exitCode3 = yield* runCommandWithExitCode(
84239
+ "git",
84240
+ "push",
84241
+ remote,
84242
+ branch,
84243
+ "--force-with-lease"
84244
+ );
84245
+ yield* mapExitCode(
84246
+ exitCode3,
84247
+ (code2) => new FailedToPushError({
84248
+ remote,
84249
+ branch,
84250
+ message: `Failed to push ${branch} to ${remote} (exit code: ${code2})`
84116
84251
  })
84117
84252
  );
84118
- });
84253
+ }
84254
+ ),
84255
+ checkoutNewBranch: Effect_exports.fn("checkoutNewBranch")(
84256
+ function* (branchName) {
84257
+ const exitCode3 = yield* runCommandWithExitCode(
84258
+ "git",
84259
+ "checkout",
84260
+ "-b",
84261
+ branchName
84262
+ );
84263
+ yield* mapExitCode(
84264
+ exitCode3,
84265
+ (code2) => new FailedToCreateBranchError({
84266
+ branchName,
84267
+ message: `Failed to create branch ${branchName} (exit code: ${code2})`
84268
+ })
84269
+ );
84270
+ }
84271
+ ),
84272
+ getLogOnelineReverse: Effect_exports.fn("getLogOnelineReverse")(
84273
+ function* (range4) {
84274
+ return yield* runCommandWithString(
84275
+ "git",
84276
+ "log",
84277
+ "--oneline",
84278
+ "--reverse",
84279
+ range4
84280
+ );
84281
+ }
84282
+ ),
84283
+ getLogOneline: Effect_exports.fn("getLogOneline")(function* (branch) {
84284
+ return yield* runCommandWithString(
84285
+ "git",
84286
+ "log",
84287
+ branch,
84288
+ "--oneline"
84289
+ );
84290
+ }),
84291
+ checkoutNewBranchAt: Effect_exports.fn("checkoutNewBranchAt")(
84292
+ function* (branchName, sha) {
84293
+ const exitCode3 = yield* runCommandWithExitCode(
84294
+ "git",
84295
+ "checkout",
84296
+ "-b",
84297
+ branchName,
84298
+ sha
84299
+ );
84300
+ yield* mapExitCode(
84301
+ exitCode3,
84302
+ (code2) => new FailedToCreateBranchError({
84303
+ branchName,
84304
+ message: `Failed to create branch ${branchName} at ${sha} (exit code: ${code2})`
84305
+ })
84306
+ );
84307
+ }
84308
+ ),
84309
+ deleteBranch: Effect_exports.fn("deleteBranch")(function* (branchName) {
84310
+ const exitCode3 = yield* runCommandWithExitCode(
84311
+ "git",
84312
+ "branch",
84313
+ "-D",
84314
+ branchName
84315
+ );
84316
+ yield* mapExitCode(
84317
+ exitCode3,
84318
+ (code2) => new FailedToDeleteBranchError({
84319
+ branchName,
84320
+ message: `Failed to delete branch ${branchName} (exit code: ${code2})`
84321
+ })
84322
+ );
84323
+ }),
84324
+ fetch: Effect_exports.fn("fetch")(function* (remote, branch) {
84325
+ const exitCode3 = yield* runCommandWithExitCode(
84326
+ "git",
84327
+ "fetch",
84328
+ remote,
84329
+ branch
84330
+ );
84331
+ yield* mapExitCode(
84332
+ exitCode3,
84333
+ (code2) => new FailedToFetchError({
84334
+ remote,
84335
+ branch,
84336
+ message: `Failed to fetch ${branch} from ${remote} (exit code: ${code2})`
84337
+ })
84338
+ );
84339
+ }),
84340
+ merge: Effect_exports.fn("merge")(function* (ref) {
84341
+ const exitCode3 = yield* runCommandWithExitCode(
84342
+ "git",
84343
+ "merge",
84344
+ ref,
84345
+ "--allow-unrelated-histories"
84346
+ );
84347
+ yield* mapExitCode(
84348
+ exitCode3,
84349
+ () => new MergeConflictError({
84350
+ ref,
84351
+ message: `Merge conflicts detected when merging ${ref}`
84352
+ })
84353
+ );
84354
+ }),
84355
+ rebase: Effect_exports.fn("rebase")(function* (onto) {
84356
+ const exitCode3 = yield* runCommandWithExitCode(
84357
+ "git",
84358
+ "rebase",
84359
+ onto
84360
+ );
84361
+ yield* mapExitCode(
84362
+ exitCode3,
84363
+ () => new RebaseConflictError({
84364
+ onto,
84365
+ message: `Rebase conflicts detected when rebasing onto ${onto}`
84366
+ })
84367
+ );
84368
+ }),
84369
+ ensureIsGitRepo: Effect_exports.fn("ensureIsGitRepo")(
84370
+ function* () {
84371
+ const cwd = config3.cwd;
84372
+ const gitDirPath = path6__namespace.join(cwd, ".git");
84373
+ const exists4 = yield* fs.exists(gitDirPath);
84374
+ if (!exists4) {
84375
+ return yield* new NotAGitRepoError({
84376
+ path: cwd,
84377
+ message: `Current directory is not a git repository: ${cwd}`
84378
+ });
84379
+ }
84380
+ }
84381
+ ),
84382
+ getUncommittedChanges: Effect_exports.fn(
84383
+ "getUncommittedChanges"
84384
+ )(function* () {
84385
+ const statusOutput = yield* runCommandWithString(
84386
+ "git",
84387
+ "status",
84388
+ "--porcelain"
84389
+ );
84119
84390
  return {
84120
- /**
84121
- * Fetches the latest changes from the origin remote.
84122
- * This updates remote-tracking branches (e.g., origin/main) without
84123
- * modifying local branches or the working directory.
84124
- *
84125
- * @returns Effect that succeeds when fetch completes
84126
- * @throws FailedToFetchOriginError if fetch fails (network, auth, or remote issues)
84127
- */
84128
- fetchOrigin: Effect_exports.fn("fetchOrigin")(function* () {
84129
- const exitCode3 = yield* runCommandWithExitCode(
84130
- "git",
84131
- "fetch",
84132
- "origin"
84133
- );
84134
- if (exitCode3 !== 0) {
84135
- return yield* Effect_exports.fail(
84136
- new FailedToFetchOriginError({
84137
- message: `Failed to fetch from origin (exit code: ${exitCode3})`
84138
- })
84139
- );
84140
- }
84141
- }),
84142
- /**
84143
- * Resolves a git ref (branch name, tag, or partial SHA) to its full SHA.
84144
- * Uses `git rev-parse` which handles all ref types.
84145
- *
84146
- * @param ref - The ref to resolve (e.g., "main", "origin/main", "HEAD", or a SHA)
84147
- * @returns The full SHA of the resolved ref
84148
- * @throws InvalidRefError if the ref cannot be resolved
84149
- */
84150
- revParse: Effect_exports.fn("revParse")(function* (ref) {
84151
- const result = yield* runCommandWithString(
84152
- "git",
84153
- "rev-parse",
84154
- ref
84155
- ).pipe(
84156
- Effect_exports.catchAll(
84157
- () => Effect_exports.fail(
84158
- new InvalidRefError({
84159
- ref,
84160
- message: `Failed to resolve ref: ${ref}`
84161
- })
84162
- )
84163
- )
84164
- );
84165
- return result;
84166
- }),
84167
- /**
84168
- * Counts the number of commits between two refs.
84169
- * Uses `git rev-list --count from..to` which counts commits reachable
84170
- * from `to` but not from `from`.
84171
- *
84172
- * @param from - Starting ref (exclusive)
84173
- * @param to - Ending ref (inclusive)
84174
- * @returns Number of commits in the range
84175
- */
84176
- revListCount: Effect_exports.fn("revListCount")(function* (from, to) {
84177
- const countOutput = yield* runCommandWithString(
84178
- "git",
84179
- "rev-list",
84180
- "--count",
84181
- `${from}..${to}`
84182
- );
84183
- return parseInt(countOutput, 10);
84184
- }),
84185
- /**
84186
- * Gets the short status output from git.
84187
- * Uses `git status --short` which outputs one line per file with
84188
- * a two-letter status code (e.g., "M " for modified, "??" for untracked).
84189
- *
84190
- * @returns The short status output as a string
84191
- */
84192
- getStatusShort: Effect_exports.fn("getStatusShort")(
84193
- function* () {
84194
- return yield* runCommandWithString(
84195
- "git",
84196
- "status",
84197
- "--short"
84198
- );
84199
- }
84200
- ),
84201
- /**
84202
- * Performs a hard reset to the specified SHA.
84203
- * WARNING: This discards all uncommitted changes and moves HEAD to the target.
84204
- * Any uncommitted work in the working directory and staging area will be lost.
84205
- *
84206
- * @param sha - The commit SHA to reset to
84207
- * @returns Effect that succeeds when reset completes
84208
- * @throws FailedToResetError if the reset fails
84209
- */
84210
- resetHard: Effect_exports.fn("resetHard")(function* (sha) {
84211
- const exitCode3 = yield* runCommandWithExitCode(
84212
- "git",
84213
- "reset",
84214
- "--hard",
84215
- sha
84216
- );
84217
- if (exitCode3 !== 0) {
84218
- return yield* Effect_exports.fail(
84219
- new FailedToResetError({
84220
- sha,
84221
- message: `Failed to reset to ${sha} (exit code: ${exitCode3})`
84222
- })
84223
- );
84224
- }
84225
- }),
84226
- /**
84227
- * Performs a soft reset of the last commit (git reset HEAD^).
84228
- * This undoes the last commit but keeps all changes in the working directory.
84229
- * The changes from the undone commit become unstaged modifications.
84230
- *
84231
- * @returns Effect that succeeds when reset completes
84232
- */
84233
- resetHead: Effect_exports.fn("resetHead")(function* () {
84234
- yield* runCommandWithExitCode("git", "reset", "HEAD^");
84235
- }),
84236
- /**
84237
- * Unstages all staged files (git restore --staged .).
84238
- * This moves files from the staging area back to unstaged state
84239
- * without discarding any modifications.
84240
- *
84241
- * @returns Effect that succeeds when restore completes
84242
- */
84243
- restoreStaged: Effect_exports.fn("restoreStaged")(function* () {
84244
- yield* runCommandWithExitCode(
84245
- "git",
84246
- "restore",
84247
- "--staged",
84248
- "."
84249
- );
84250
- }),
84251
- /**
84252
- * Stages all changes in the working directory (git add .).
84253
- * This adds all modified, deleted, and new files to the staging area.
84254
- *
84255
- * @returns Effect that succeeds when staging completes
84256
- */
84257
- stageAll: Effect_exports.fn("stageAll")(function* () {
84258
- yield* runCommandWithExitCode("git", "add", ".");
84259
- }),
84260
- /**
84261
- * Creates a new commit with the specified message.
84262
- * All currently staged changes will be included in the commit.
84263
- *
84264
- * @param message - The commit message
84265
- * @returns Effect that succeeds when commit completes
84266
- * @throws FailedToCommitError if the commit fails (e.g., nothing staged, hook failure)
84267
- */
84268
- commit: Effect_exports.fn("commit")(function* (message) {
84269
- const exitCode3 = yield* runCommandWithExitCode(
84270
- "git",
84271
- "commit",
84272
- "-m",
84273
- message
84274
- );
84275
- if (exitCode3 !== 0) {
84276
- return yield* Effect_exports.fail(
84277
- new FailedToCommitError({
84278
- message: `Failed to commit (exit code: ${exitCode3})`
84279
- })
84280
- );
84281
- }
84282
- }),
84283
- /**
84284
- * Cherry-picks a range of commits onto the current branch.
84285
- * The range is specified as "from..to" where commits after "from"
84286
- * up to and including "to" are applied.
84287
- *
84288
- * @param range - The commit range to cherry-pick (e.g., "abc123..def456")
84289
- * @returns Effect that succeeds when cherry-pick completes
84290
- * @throws CherryPickConflictError if conflicts occur during cherry-pick
84291
- */
84292
- cherryPick: Effect_exports.fn("cherryPick")(function* (range4) {
84293
- const exitCode3 = yield* runCommandWithExitCode(
84294
- "git",
84295
- "cherry-pick",
84296
- range4
84297
- );
84298
- if (exitCode3 !== 0) {
84299
- return yield* Effect_exports.fail(
84300
- new CherryPickConflictError({
84301
- range: range4,
84302
- message: `Cherry-pick conflict on range ${range4}`
84303
- })
84304
- );
84305
- }
84306
- }),
84307
- /**
84308
- * Continues a cherry-pick after conflicts have been resolved.
84309
- * Call this after manually resolving conflicts and staging the changes.
84310
- *
84311
- * @returns Effect that succeeds when cherry-pick continues, or fails if more conflicts
84312
- * @throws CherryPickConflictError if additional conflicts occur
84313
- */
84314
- cherryPickContinue: Effect_exports.fn("cherryPickContinue")(
84315
- function* () {
84316
- const exitCode3 = yield* runCommandWithExitCode(
84317
- "git",
84318
- "cherry-pick",
84319
- "--continue"
84320
- );
84321
- if (exitCode3 !== 0) {
84322
- return yield* Effect_exports.fail(
84323
- new CherryPickConflictError({
84324
- range: "continue",
84325
- message: "Cherry-pick continue encountered conflicts"
84326
- })
84327
- );
84328
- }
84329
- }
84330
- ),
84331
- /**
84332
- * Aborts an in-progress cherry-pick operation.
84333
- * This restores the branch to its state before the cherry-pick started.
84334
- *
84335
- * @returns Effect that succeeds when abort completes
84336
- */
84337
- cherryPickAbort: Effect_exports.fn("cherryPickAbort")(
84338
- function* () {
84339
- yield* runCommandWithExitCode(
84340
- "git",
84341
- "cherry-pick",
84342
- "--abort"
84343
- );
84344
- }
84345
- ),
84346
- /**
84347
- * Switches to an existing branch.
84348
- * The working directory must be clean or have changes that don't
84349
- * conflict with the target branch.
84350
- *
84351
- * @param branch - The branch name to switch to
84352
- * @returns Effect that succeeds when checkout completes
84353
- * @throws FailedToCheckoutError if checkout fails (branch not found, conflicts)
84354
- */
84355
- checkout: Effect_exports.fn("checkout")(function* (branch) {
84356
- const exitCode3 = yield* runCommandWithExitCode(
84357
- "git",
84358
- "checkout",
84359
- branch
84360
- );
84361
- if (exitCode3 !== 0) {
84362
- return yield* Effect_exports.fail(
84363
- new FailedToCheckoutError({
84364
- branch,
84365
- message: `Failed to checkout ${branch} (exit code: ${exitCode3})`
84366
- })
84367
- );
84368
- }
84369
- }),
84370
- /**
84371
- * Force pushes a branch to a remote using --force-with-lease.
84372
- * This is safer than --force because it will fail if the remote has
84373
- * commits that aren't in your local branch, preventing accidental
84374
- * overwrites of others' work.
84375
- *
84376
- * @param remote - The remote name (e.g., "origin")
84377
- * @param branch - The branch to push
84378
- * @returns Effect that succeeds when push completes
84379
- * @throws FailedToPushError if push fails (rejected, network, auth issues)
84380
- */
84381
- pushForceWithLease: Effect_exports.fn("pushForceWithLease")(
84382
- function* (remote, branch) {
84383
- const exitCode3 = yield* runCommandWithExitCode(
84384
- "git",
84385
- "push",
84386
- remote,
84387
- branch,
84388
- "--force-with-lease"
84389
- );
84390
- if (exitCode3 !== 0) {
84391
- return yield* Effect_exports.fail(
84392
- new FailedToPushError({
84393
- remote,
84394
- branch,
84395
- message: `Failed to push ${branch} to ${remote} (exit code: ${exitCode3})`
84396
- })
84397
- );
84398
- }
84399
- }
84400
- ),
84401
- /**
84402
- * Creates and switches to a new branch (git checkout -b).
84403
- * The branch is created at the current HEAD position.
84404
- *
84405
- * @param branchName - The name of the new branch to create
84406
- * @returns Effect that succeeds when branch is created and checked out
84407
- * @throws FailedToCreateBranchError if branch creation fails (e.g., branch already exists)
84408
- */
84409
- checkoutNewBranch: Effect_exports.fn("checkoutNewBranch")(
84410
- function* (branchName) {
84411
- const exitCode3 = yield* runCommandWithExitCode(
84412
- "git",
84413
- "checkout",
84414
- "-b",
84415
- branchName
84416
- );
84417
- if (exitCode3 !== 0) {
84418
- return yield* Effect_exports.fail(
84419
- new FailedToCreateBranchError({
84420
- branchName,
84421
- message: `Failed to create branch ${branchName} (exit code: ${exitCode3})`
84422
- })
84423
- );
84424
- }
84425
- }
84426
- ),
84427
- /**
84428
- * Gets commit log in oneline format, reversed (oldest first).
84429
- * Uses `git log --oneline --reverse` with a range specifier.
84430
- * Each line contains: <short-sha> <commit-message>
84431
- *
84432
- * @param range - The commit range (e.g., "main..feature" or "HEAD~5..HEAD")
84433
- * @returns The commit log output as a string
84434
- */
84435
- getLogOnelineReverse: Effect_exports.fn("getLogOnelineReverse")(
84436
- function* (range4) {
84437
- return yield* runCommandWithString(
84438
- "git",
84439
- "log",
84440
- "--oneline",
84441
- "--reverse",
84442
- range4
84443
- );
84444
- }
84445
- ),
84446
- /**
84447
- * Gets commit log in oneline format for a branch.
84448
- * Uses `git log <branch> --oneline`.
84449
- * Each line contains: <short-sha> <commit-message>
84450
- *
84451
- * @param branch - The branch to get log for (e.g., "main", "HEAD", "origin/main")
84452
- * @returns The commit log output as a string
84453
- */
84454
- getLogOneline: Effect_exports.fn("getLogOneline")(function* (branch) {
84455
- return yield* runCommandWithString(
84456
- "git",
84457
- "log",
84458
- branch,
84459
- "--oneline"
84460
- );
84461
- }),
84462
- /**
84463
- * Gets the parent commit SHA of a given commit.
84464
- * Uses `git rev-parse <sha>^` to resolve the parent ref.
84465
- *
84466
- * @param sha - The commit SHA to get the parent of
84467
- * @returns The full SHA of the parent commit
84468
- * @throws NoParentCommitError if the commit has no parent (e.g., initial commit)
84469
- */
84470
- getParentCommit: Effect_exports.fn("getParentCommit")(function* (sha) {
84471
- const parentSha = yield* runCommandWithString(
84472
- "git",
84473
- "rev-parse",
84474
- `${sha}^`
84475
- ).pipe(
84476
- Effect_exports.catchAll(
84477
- () => Effect_exports.fail(
84478
- new NoParentCommitError({ commitSha: sha })
84479
- )
84480
- )
84481
- );
84482
- return parentSha;
84483
- }),
84484
- /**
84485
- * Creates and switches to a new branch at a specific commit (git checkout -b <name> <sha>).
84486
- * Unlike checkoutNewBranch which creates at HEAD, this creates the branch
84487
- * at an arbitrary commit position.
84488
- *
84489
- * @param branchName - The name of the new branch to create
84490
- * @param sha - The commit SHA where the branch should be created
84491
- * @returns Effect that succeeds when branch is created and checked out
84492
- * @throws FailedToCreateBranchError if branch creation fails (e.g., branch already exists)
84493
- */
84494
- checkoutNewBranchAt: Effect_exports.fn("checkoutNewBranchAt")(
84495
- function* (branchName, sha) {
84496
- const exitCode3 = yield* runCommandWithExitCode(
84497
- "git",
84498
- "checkout",
84499
- "-b",
84500
- branchName,
84501
- sha
84502
- );
84503
- if (exitCode3 !== 0) {
84504
- return yield* Effect_exports.fail(
84505
- new FailedToCreateBranchError({
84506
- branchName,
84507
- message: `Failed to create branch ${branchName} at ${sha} (exit code: ${exitCode3})`
84508
- })
84509
- );
84510
- }
84511
- }
84512
- ),
84513
- /**
84514
- * Deletes a local branch.
84515
- * Uses `git branch -D` which force deletes even if not fully merged.
84516
- *
84517
- * @param branchName - The name of the branch to delete
84518
- * @returns Effect that succeeds when branch is deleted
84519
- * @throws FailedToDeleteBranchError if deletion fails
84520
- */
84521
- deleteBranch: Effect_exports.fn("deleteBranch")(function* (branchName) {
84522
- const exitCode3 = yield* runCommandWithExitCode(
84523
- "git",
84524
- "branch",
84525
- "-D",
84526
- branchName
84527
- );
84528
- if (exitCode3 !== 0) {
84529
- return yield* Effect_exports.fail(
84530
- new FailedToDeleteBranchError({
84531
- branchName,
84532
- message: `Failed to delete branch ${branchName} (exit code: ${exitCode3})`
84533
- })
84534
- );
84535
- }
84536
- }),
84537
- /**
84538
- * Fetches a specific branch from a remote.
84539
- * Updates the remote-tracking branch (e.g., origin/main) without
84540
- * modifying local branches or the working directory.
84541
- *
84542
- * @param remote - The remote name (e.g., "origin", "upstream")
84543
- * @param branch - The branch to fetch (e.g., "main")
84544
- * @returns Effect that succeeds when fetch completes
84545
- * @throws FailedToFetchError if fetch fails (network, auth, or remote/branch not found)
84546
- */
84547
- fetch: Effect_exports.fn("fetch")(function* (remote, branch) {
84548
- const exitCode3 = yield* runCommandWithExitCode(
84549
- "git",
84550
- "fetch",
84551
- remote,
84552
- branch
84553
- );
84554
- if (exitCode3 !== 0) {
84555
- return yield* Effect_exports.fail(
84556
- new FailedToFetchError({
84557
- remote,
84558
- branch,
84559
- message: `Failed to fetch ${branch} from ${remote} (exit code: ${exitCode3})`
84560
- })
84561
- );
84562
- }
84563
- }),
84564
- /**
84565
- * Merges a ref into the current branch.
84566
- * This creates a merge commit if necessary, or performs a fast-forward
84567
- * merge if the current branch is directly behind the target ref.
84568
- *
84569
- * @param ref - The ref to merge (e.g., "origin/main", a branch name, or SHA)
84570
- * @returns Effect that succeeds when merge completes
84571
- * @throws MergeConflictError if conflicts occur during merge
84572
- */
84573
- merge: Effect_exports.fn("merge")(function* (ref) {
84574
- const exitCode3 = yield* runCommandWithExitCode(
84575
- "git",
84576
- "merge",
84577
- ref
84578
- );
84579
- if (exitCode3 !== 0) {
84580
- return yield* Effect_exports.fail(
84581
- new MergeConflictError({
84582
- ref,
84583
- message: `Merge conflicts detected when merging ${ref}`
84584
- })
84585
- );
84586
- }
84587
- }),
84588
- /**
84589
- * Rebases the current branch onto another branch.
84590
- * This replays all commits from the current branch on top of the target branch.
84591
- * WARNING: Rebase rewrites commit history - all rebased commits get new SHAs.
84592
- * Never rebase commits that have been pushed to a shared remote unless you
84593
- * coordinate with all collaborators.
84594
- *
84595
- * @param onto - The branch to rebase onto (e.g., "main", "origin/main")
84596
- * @returns Effect that succeeds when rebase completes
84597
- * @throws RebaseConflictError if conflicts occur during rebase
84598
- */
84599
- rebase: Effect_exports.fn("rebase")(function* (onto) {
84600
- const exitCode3 = yield* runCommandWithExitCode(
84601
- "git",
84602
- "rebase",
84603
- onto
84604
- );
84605
- if (exitCode3 !== 0) {
84606
- return yield* Effect_exports.fail(
84607
- new RebaseConflictError({
84608
- onto,
84609
- message: `Rebase conflicts detected when rebasing onto ${onto}`
84610
- })
84611
- );
84612
- }
84613
- }),
84614
- ensureIsGitRepo: Effect_exports.fn("ensureIsGitRepo")(
84615
- function* () {
84616
- const cwd = config3.cwd;
84617
- const gitDirPath = path6__namespace.join(cwd, ".git");
84618
- const exists4 = yield* fs.exists(gitDirPath);
84619
- if (!exists4) {
84620
- return yield* new NotAGitRepoError({
84621
- path: cwd,
84622
- message: `Current directory is not a git repository: ${cwd}`
84623
- });
84624
- }
84625
- }
84626
- ),
84627
- getUncommittedChanges: Effect_exports.fn(
84628
- "getUncommittedChanges"
84629
- )(function* () {
84630
- const statusOutput = yield* runCommandWithString(
84631
- "git",
84632
- "status",
84633
- "--porcelain"
84634
- );
84635
- return {
84636
- hasUncommittedChanges: statusOutput !== "",
84637
- statusOutput
84638
- };
84639
- }),
84640
- /**
84641
- * This command needs to be run in every user-facing command that
84642
- * interacts with a branch. It ensures that the branch is connected
84643
- * to the upstream repository and that the local branch is tracking
84644
- * the upstream branch.
84645
- *
84646
- * This is to handle the case where the user has a fork of the
84647
- * upstream repository.
84648
- */
84649
- ensureUpstreamBranchConnected: Effect_exports.fn(
84650
- "ensureUpstreamBranchConnected"
84651
- )(function* (opts) {
84652
- const { targetBranch } = opts;
84653
- const { remoteName } = yield* detectUpstreamRemote();
84654
- const fetchExitCode = yield* runCommandWithExitCode(
84655
- "git",
84656
- "fetch",
84657
- remoteName,
84658
- targetBranch
84659
- );
84660
- if (fetchExitCode !== 0) {
84661
- return yield* Effect_exports.fail(
84662
- new FailedToFetchUpstreamError({
84663
- targetBranch,
84664
- message: `Failed to fetch upstream: ${fetchExitCode}`
84665
- })
84666
- );
84667
- }
84391
+ hasUncommittedChanges: statusOutput !== "",
84392
+ statusOutput
84393
+ };
84394
+ }),
84395
+ ensureUpstreamBranchConnected: Effect_exports.fn(
84396
+ "ensureUpstreamBranchConnected"
84397
+ )(function* (opts) {
84398
+ const { targetBranch } = opts;
84399
+ const fetchExitCode = yield* runCommandWithExitCode(
84400
+ "git",
84401
+ "fetch",
84402
+ "upstream",
84403
+ targetBranch
84404
+ );
84405
+ if (fetchExitCode !== 0) {
84406
+ return yield* Effect_exports.fail(
84407
+ new FailedToFetchUpstreamError({
84408
+ targetBranch,
84409
+ message: `Failed to fetch upstream: ${fetchExitCode}`
84410
+ })
84411
+ );
84412
+ }
84413
+ yield* runCommandWithExitCode(
84414
+ "git",
84415
+ "branch",
84416
+ "-D",
84417
+ targetBranch
84418
+ );
84419
+ const trackBranchExitCode = yield* runCommandWithExitCode(
84420
+ "git",
84421
+ "branch",
84422
+ "--track",
84423
+ targetBranch,
84424
+ `upstream/${targetBranch}`
84425
+ );
84426
+ if (trackBranchExitCode !== 0) {
84427
+ return yield* Effect_exports.fail(
84428
+ new FailedToTrackBranchError({
84429
+ branchName: targetBranch,
84430
+ message: `Failed to track branch.`
84431
+ })
84432
+ );
84433
+ }
84434
+ }),
84435
+ getCurrentBranch: Effect_exports.fn("getCurrentBranch")(
84436
+ function* () {
84437
+ const currentBranch = (yield* runCommandWithString(
84438
+ "git",
84439
+ "branch",
84440
+ "--show-current"
84441
+ )).trim();
84442
+ return currentBranch;
84443
+ }
84444
+ ),
84445
+ setUpstreamRemote: Effect_exports.fn("setUpstreamRemote")(
84446
+ function* (url2) {
84447
+ const setUrlExitCode = yield* runCommandWithExitCode(
84448
+ "git",
84449
+ "remote",
84450
+ "set-url",
84451
+ "upstream",
84452
+ url2
84453
+ );
84454
+ if (setUrlExitCode !== 0) {
84668
84455
  yield* runCommandWithExitCode(
84669
84456
  "git",
84670
- "branch",
84671
- "-D",
84672
- targetBranch
84673
- );
84674
- const trackBranchExitCode = yield* runCommandWithExitCode(
84675
- "git",
84676
- "branch",
84677
- "--track",
84678
- targetBranch,
84679
- `${remoteName}/${targetBranch}`
84457
+ "remote",
84458
+ "add",
84459
+ "upstream",
84460
+ url2
84680
84461
  );
84681
- if (trackBranchExitCode !== 0) {
84682
- return yield* Effect_exports.fail(
84683
- new FailedToTrackBranchError({
84684
- branchName: targetBranch,
84685
- message: `Failed to track branch.`
84686
- })
84687
- );
84688
- }
84689
- }),
84690
- getCurrentBranch: Effect_exports.fn("getCurrentBranch")(
84691
- function* () {
84692
- const currentBranch = (yield* runCommandWithString(
84693
- "git",
84694
- "branch",
84695
- "--show-current"
84696
- )).trim();
84697
- return currentBranch;
84698
- }
84699
- ),
84700
- detectUpstreamRemote
84701
- };
84702
- }),
84462
+ }
84463
+ }
84464
+ )
84465
+ };
84466
+ });
84467
+ var GitService = class extends Effect_exports.Service()(
84468
+ "GitService",
84469
+ {
84470
+ effect: makeGitService,
84703
84471
  dependencies: [
84704
84472
  NodeFileSystem_exports2.layer,
84705
84473
  defaultGitServiceConfigLayer
@@ -84750,7 +84518,6 @@ var normalizeExerciseNumber = (str) => {
84750
84518
  var PromptService = class extends Effect_exports.Service()(
84751
84519
  "PromptService",
84752
84520
  {
84753
- // eslint-disable-next-line require-yield
84754
84521
  effect: Effect_exports.gen(function* () {
84755
84522
  const confirmReadyToCommit = Effect_exports.fn(
84756
84523
  "confirmReadyToCommit"
@@ -84822,27 +84589,6 @@ var PromptService = class extends Effect_exports.Service()(
84822
84589
  );
84823
84590
  return action;
84824
84591
  });
84825
- const selectProblemOrSolution = Effect_exports.fn(
84826
- "selectProblemOrSolution"
84827
- )(function* () {
84828
- const { action } = yield* runPrompt(
84829
- () => (0, import_prompts.prompt)([
84830
- {
84831
- type: "select",
84832
- name: "action",
84833
- message: "What would you like to do?",
84834
- choices: [
84835
- {
84836
- title: "Start the exercise",
84837
- value: "problem"
84838
- },
84839
- { title: "View final code", value: "solution" }
84840
- ]
84841
- }
84842
- ])
84843
- );
84844
- return action;
84845
- });
84846
84592
  const selectResetAction = Effect_exports.fn("selectResetAction")(
84847
84593
  function* (_branch) {
84848
84594
  const { action } = yield* runPrompt(
@@ -85013,14 +84759,17 @@ var PromptService = class extends Effect_exports.Service()(
85013
84759
  type: "autocomplete",
85014
84760
  name: "subfolderIndex",
85015
84761
  message: "Select a subfolder",
85016
- choices: subfolders.map((subfolder, index) => ({
84762
+ choices: subfolders.map((subfolder) => ({
85017
84763
  title: subfolder,
85018
- value: index
84764
+ // Use subfolder name as value instead of numeric index
84765
+ // to avoid prompts library treating 0 as falsy and
84766
+ // returning the title string instead (GitHub issue #42)
84767
+ value: subfolder
85019
84768
  }))
85020
84769
  }
85021
84770
  ])
85022
84771
  );
85023
- return subfolderIndex;
84772
+ return subfolders.indexOf(subfolderIndex);
85024
84773
  }
85025
84774
  );
85026
84775
  const selectExerciseAction = Effect_exports.fn(
@@ -85131,7 +84880,6 @@ var PromptService = class extends Effect_exports.Service()(
85131
84880
  confirmSaveToTargetBranch,
85132
84881
  confirmForcePush,
85133
84882
  selectCherryPickConflictAction,
85134
- selectProblemOrSolution,
85135
84883
  selectResetAction,
85136
84884
  confirmResetWithUncommittedChanges,
85137
84885
  inputBranchName,
@@ -85257,10 +85005,6 @@ var selectLessonCommit = ({
85257
85005
  lessonId: selectedLessonId
85258
85006
  };
85259
85007
  });
85260
- var getParentCommit = ({ commitSha }) => Effect_exports.gen(function* () {
85261
- const gitService = yield* GitService;
85262
- return yield* gitService.getParentCommit(commitSha);
85263
- });
85264
85008
 
85265
85009
  // src/constants.ts
85266
85010
  var DEFAULT_PROJECT_TARGET_BRANCH = "live-run-through";
@@ -85288,11 +85032,13 @@ var InvalidBranchOperationError = class extends Data_exports.TaggedError(
85288
85032
  };
85289
85033
  var runCherryPick = ({
85290
85034
  branch,
85291
- lessonId
85035
+ lessonId,
85036
+ upstream
85292
85037
  }) => Effect_exports.gen(function* () {
85293
85038
  const git = yield* GitService;
85294
85039
  const promptService = yield* PromptService;
85295
85040
  yield* git.ensureIsGitRepo();
85041
+ yield* git.setUpstreamRemote(upstream);
85296
85042
  yield* git.ensureUpstreamBranchConnected({
85297
85043
  targetBranch: branch
85298
85044
  });
@@ -85342,9 +85088,15 @@ var cherryPick = Command_exports.make(
85342
85088
  ),
85343
85089
  Options_exports.withDefault(DEFAULT_PROJECT_TARGET_BRANCH)
85344
85090
  ),
85091
+ upstream: Options_exports.text("upstream").pipe(
85092
+ Options_exports.withDescription(
85093
+ "Git URL or local path to the upstream exercise repo"
85094
+ )
85095
+ ),
85345
85096
  cwd: cwdOption
85346
85097
  },
85347
- ({ branch, cwd, lessonId }) => runCherryPick({ branch, lessonId }).pipe(
85098
+ /* v8 ignore start - CLI error handlers are presentation logic */
85099
+ ({ branch, cwd, lessonId, upstream }) => runCherryPick({ branch, lessonId, upstream }).pipe(
85348
85100
  Effect_exports.provideService(
85349
85101
  GitServiceConfig,
85350
85102
  GitServiceConfig.of({
@@ -85358,12 +85110,6 @@ var cherryPick = Command_exports.make(
85358
85110
  process.exitCode = 1;
85359
85111
  });
85360
85112
  },
85361
- NoUpstreamFoundError: (error4) => {
85362
- return Effect_exports.gen(function* () {
85363
- yield* Console_exports.error(`Error: ${error4.message}`);
85364
- process.exitCode = 1;
85365
- });
85366
- },
85367
85113
  CommitNotFoundError: (error4) => {
85368
85114
  return Effect_exports.gen(function* () {
85369
85115
  yield* Console_exports.error(
@@ -85402,6 +85148,7 @@ var cherryPick = Command_exports.make(
85402
85148
  });
85403
85149
  })
85404
85150
  )
85151
+ /* v8 ignore stop */
85405
85152
  ).pipe(
85406
85153
  Command_exports.withDescription(
85407
85154
  "Cherry-pick a specific lesson commit onto current branch"
@@ -86084,6 +85831,7 @@ var updateCVM = Command_exports.make(
86084
85831
  );
86085
85832
  var createErrorTracker = () => {
86086
85833
  const groupedErrors = {};
85834
+ const errors = [];
86087
85835
  return {
86088
85836
  addError: (lesson, error4) => {
86089
85837
  if (!groupedErrors[lesson.sectionPath]) {
@@ -86093,20 +85841,21 @@ var createErrorTracker = () => {
86093
85841
  groupedErrors[lesson.sectionPath][lesson.path] = [];
86094
85842
  }
86095
85843
  groupedErrors[lesson.sectionPath][lesson.path].push(error4);
85844
+ errors.push({
85845
+ sectionPath: lesson.sectionPath,
85846
+ lessonPath: lesson.path,
85847
+ error: error4
85848
+ });
86096
85849
  },
85850
+ getErrors: () => errors,
85851
+ /* v8 ignore start */
86097
85852
  log: Effect_exports.gen(function* () {
86098
- for (const [section, exercises] of Object.entries(
86099
- groupedErrors
86100
- )) {
85853
+ for (const [section, exercises] of Object.entries(groupedErrors)) {
86101
85854
  yield* Console_exports.log(util.styleText(["bold"], section));
86102
- for (const [exercise2, errors] of Object.entries(
86103
- exercises
86104
- )) {
85855
+ for (const [exercise2, errors2] of Object.entries(exercises)) {
86105
85856
  yield* Console_exports.log(` ${exercise2}`);
86106
- for (const error4 of errors) {
86107
- yield* Console_exports.log(
86108
- util.styleText(["red"], ` ${error4}`)
86109
- );
85857
+ for (const error4 of errors2) {
85858
+ yield* Console_exports.log(util.styleText(["red"], ` ${error4}`));
86110
85859
  }
86111
85860
  }
86112
85861
  }
@@ -86114,177 +85863,183 @@ var createErrorTracker = () => {
86114
85863
  process.exitCode = 1;
86115
85864
  }
86116
85865
  })
85866
+ /* v8 ignore stop */
86117
85867
  };
86118
85868
  };
86119
- var lint = Command_exports.make(
86120
- "lint",
86121
- {
86122
- root: rootOption,
86123
- cwd: cwdOption
86124
- },
86125
- Effect_exports.fn("lint")(
86126
- function* ({ cwd, root }) {
86127
- const existsCache = yield* Cache_exports.make({
86128
- capacity: 1e4,
86129
- timeToLive: Duration_exports.infinity,
86130
- lookup: (key) => Effect_exports.gen(function* () {
86131
- const exists4 = yield* fs.exists(key);
86132
- return exists4;
86133
- })
86134
- });
86135
- const allReadmeContents = [];
86136
- const errorTracker = createErrorTracker();
86137
- const lessonParser = yield* LessonParserService;
86138
- const fs = yield* FileSystem_exports.FileSystem;
86139
- const lessons = yield* lessonParser.getLessonsFromRepo(
86140
- root
85869
+ var runLint = (opts) => Effect_exports.gen(function* () {
85870
+ const lessonParser = yield* LessonParserService;
85871
+ const fs = yield* FileSystem_exports.FileSystem;
85872
+ const existsCache = yield* Cache_exports.make({
85873
+ capacity: 1e4,
85874
+ timeToLive: Duration_exports.infinity,
85875
+ lookup: (key) => Effect_exports.gen(function* () {
85876
+ const exists4 = yield* fs.exists(key);
85877
+ return exists4;
85878
+ })
85879
+ });
85880
+ const allReadmeContents = [];
85881
+ const errorTracker = createErrorTracker();
85882
+ const lessons = yield* lessonParser.getLessonsFromRepo(opts.root);
85883
+ for (const lesson of lessons) {
85884
+ const subfolders = yield* lesson.subfolders();
85885
+ if (subfolders.length === 0) {
85886
+ errorTracker.addError(
85887
+ lesson,
85888
+ "No subfolders, like problem or solution, found in the exercise."
85889
+ );
85890
+ continue;
85891
+ }
85892
+ const folderForReadme = subfolders.find(
85893
+ (folder) => folder === "problem" || folder === "explainer" || folder === "explainer.1"
85894
+ );
85895
+ if (!folderForReadme) {
85896
+ errorTracker.addError(
85897
+ lesson,
85898
+ "No problem, explainer, or explainer.1 folder found in the exercise."
86141
85899
  );
86142
- for (const lesson of lessons) {
86143
- const subfolders = yield* lesson.subfolders();
86144
- if (subfolders.length === 0) {
85900
+ continue;
85901
+ }
85902
+ const readmePath = path6__namespace.join(
85903
+ lesson.absolutePath(),
85904
+ folderForReadme,
85905
+ "readme.md"
85906
+ );
85907
+ const readmeExists = yield* existsCache.get(readmePath);
85908
+ if (!readmeExists) {
85909
+ errorTracker.addError(
85910
+ lesson,
85911
+ "readme.md file not found in the exercise."
85912
+ );
85913
+ } else {
85914
+ const readmeContent = yield* fs.readFileString(readmePath);
85915
+ allReadmeContents.push(readmeContent);
85916
+ if (readmeContent.trim().length === 0) {
85917
+ errorTracker.addError(lesson, "readme.md file is empty");
85918
+ continue;
85919
+ }
85920
+ if (readmeContent.includes("`pnpm run exercise ")) {
85921
+ errorTracker.addError(
85922
+ lesson,
85923
+ "readme.md file contains a pnpm run exercise command. Please remove it."
85924
+ );
85925
+ continue;
85926
+ }
85927
+ const absoluteLinks = readmeContent.match(/\[[^\]]+\]\(\/[^)]+\)/gm) ?? [];
85928
+ for (const link3 of absoluteLinks) {
85929
+ const splitResult = link3.split("](");
85930
+ const url2 = splitResult[1]?.slice(1, -1);
85931
+ if (!url2) continue;
85932
+ const linkExists = yield* existsCache.get(path6__namespace.join(opts.cwd, url2));
85933
+ if (!linkExists) {
86145
85934
  errorTracker.addError(
86146
85935
  lesson,
86147
- "No subfolders, like problem or solution, found in the exercise."
85936
+ `Broken absolute link in readme.md: ${url2}`
86148
85937
  );
86149
- continue;
86150
85938
  }
86151
- const folderForReadme = subfolders.find(
86152
- (folder) => folder === "problem" || folder === "explainer" || folder === "explainer.1"
85939
+ }
85940
+ const relativeLinks = readmeContent.match(/\[[^\]]+\]\(\.\/[^)]+\)/gm) ?? [];
85941
+ for (const link3 of relativeLinks) {
85942
+ const splitResult = link3.split("](");
85943
+ const url2 = splitResult[1]?.slice(0, -1);
85944
+ if (!url2) continue;
85945
+ const linkExists = yield* existsCache.get(
85946
+ path6__namespace.resolve(lesson.absolutePath(), folderForReadme, url2)
86153
85947
  );
86154
- if (!folderForReadme) {
85948
+ if (!linkExists) {
86155
85949
  errorTracker.addError(
86156
85950
  lesson,
86157
- "No problem, explainer, or explainer.1 folder found in the exercise."
85951
+ `Broken relative link in readme.md: ${url2}`
86158
85952
  );
86159
- continue;
86160
85953
  }
86161
- const readmePath = path6__namespace.join(
85954
+ }
85955
+ }
85956
+ for (const subfolder of subfolders) {
85957
+ const mainFilePath = path6__namespace.join(
85958
+ lesson.absolutePath(),
85959
+ subfolder,
85960
+ "main.ts"
85961
+ );
85962
+ const mainFileExists = yield* existsCache.get(mainFilePath);
85963
+ if (!mainFileExists) {
85964
+ const readmeInSubfolderPath = path6__namespace.join(
86162
85965
  lesson.absolutePath(),
86163
- folderForReadme,
85966
+ subfolder,
86164
85967
  "readme.md"
86165
85968
  );
86166
- const readmeExists = yield* existsCache.get(readmePath);
86167
- if (!readmeExists) {
86168
- errorTracker.addError(
86169
- lesson,
86170
- "readme.md file not found in the exercise."
86171
- );
86172
- } else {
86173
- const readmeContent = yield* fs.readFileString(
86174
- readmePath
86175
- );
86176
- allReadmeContents.push(readmeContent);
86177
- if (readmeContent.trim().length === 0) {
86178
- errorTracker.addError(
86179
- lesson,
86180
- "readme.md file is empty"
86181
- );
86182
- continue;
86183
- }
86184
- if (readmeContent.includes("`pnpm run exercise ")) {
86185
- errorTracker.addError(
86186
- lesson,
86187
- "readme.md file contains a pnpm run exercise command. Please remove it."
86188
- );
86189
- continue;
86190
- }
86191
- const absoluteLinks = readmeContent.match(/\[[^\]]+\]\(\/[^)]+\)/gm) ?? [];
86192
- for (const link3 of absoluteLinks) {
86193
- const splitResult = link3.split("](");
86194
- const url2 = splitResult[1]?.slice(1, -1);
86195
- if (!url2) continue;
86196
- const linkExists = yield* existsCache.get(
86197
- path6__namespace.join(cwd, url2)
86198
- );
86199
- if (!linkExists) {
86200
- errorTracker.addError(
86201
- lesson,
86202
- `Broken absolute link in readme.md: ${url2}`
86203
- );
86204
- }
86205
- }
86206
- const relativeLinks = readmeContent.match(/\[[^\]]+\]\(\.\/[^)]+\)/gm) ?? [];
86207
- for (const link3 of relativeLinks) {
86208
- const splitResult = link3.split("](");
86209
- const url2 = splitResult[1]?.slice(0, -1);
86210
- if (!url2) continue;
86211
- const linkExists = yield* existsCache.get(
86212
- path6__namespace.resolve(
86213
- lesson.absolutePath(),
86214
- folderForReadme,
86215
- url2
86216
- )
86217
- );
86218
- if (!linkExists) {
86219
- errorTracker.addError(
86220
- lesson,
86221
- `Broken relative link in readme.md: ${url2}`
86222
- );
86223
- }
86224
- }
86225
- }
86226
- for (const subfolder of subfolders) {
86227
- const mainFilePath = path6__namespace.join(
86228
- lesson.absolutePath(),
86229
- subfolder,
86230
- "main.ts"
86231
- );
86232
- const mainFileExists = yield* existsCache.get(
86233
- mainFilePath
86234
- );
86235
- if (!mainFileExists) {
86236
- const readmeInSubfolderPath = path6__namespace.join(
86237
- lesson.absolutePath(),
86238
- subfolder,
86239
- "readme.md"
86240
- );
86241
- const readmeInSubfolderExists = yield* existsCache.get(readmeInSubfolderPath);
86242
- if (!readmeInSubfolderExists) {
86243
- errorTracker.addError(
86244
- lesson,
86245
- `main.ts file not found in the ${subfolder} folder.`
86246
- );
86247
- }
86248
- } else {
86249
- const mainFileContent = yield* fs.readFileString(
86250
- mainFilePath
86251
- );
86252
- const lineCount = mainFileContent.trim().split("\n").length;
86253
- if (lineCount === 1) {
86254
- errorTracker.addError(
86255
- lesson,
86256
- `main.ts file in the ${subfolder} folder is too short (${lineCount} lines). Please use a readme-only exercise instead if you only need to show instructions.`
86257
- );
86258
- }
86259
- }
86260
- }
86261
- const files = yield* lesson.allFiles();
86262
- if (files.some((file6) => file6.includes(".gitkeep"))) {
85969
+ const readmeInSubfolderExists = yield* existsCache.get(readmeInSubfolderPath);
85970
+ if (!readmeInSubfolderExists) {
86263
85971
  errorTracker.addError(
86264
85972
  lesson,
86265
- ".gitkeep file found in the exercise."
85973
+ `main.ts file not found in the ${subfolder} folder.`
86266
85974
  );
86267
85975
  }
86268
- if (files.some((file6) => file6.includes("speaker-notes.md"))) {
85976
+ } else {
85977
+ const mainFileContent = yield* fs.readFileString(mainFilePath);
85978
+ const lineCount = mainFileContent.trim().split("\n").length;
85979
+ if (lineCount === 1) {
86269
85980
  errorTracker.addError(
86270
85981
  lesson,
86271
- "speaker-notes.md file found in the exercise. This should not be exposed to users."
85982
+ `main.ts file in the ${subfolder} folder is too short (${lineCount} lines). Please use a readme-only exercise instead if you only need to show instructions.`
86272
85983
  );
86273
85984
  }
86274
85985
  }
86275
- const readmeContents = allReadmeContents.join("\n");
86276
- const referenceLessons = lessons.filter(
86277
- (lesson) => lesson.sectionName === "reference"
85986
+ }
85987
+ const files = yield* lesson.allFiles();
85988
+ if (files.some((file6) => file6.includes(".gitkeep"))) {
85989
+ errorTracker.addError(lesson, ".gitkeep file found in the exercise.");
85990
+ }
85991
+ if (files.some((file6) => file6.includes("speaker-notes.md"))) {
85992
+ errorTracker.addError(
85993
+ lesson,
85994
+ "speaker-notes.md file found in the exercise. This should not be exposed to users."
86278
85995
  );
86279
- for (const referenceLesson of referenceLessons) {
86280
- if (!readmeContents.includes(referenceLesson.path)) {
86281
- errorTracker.addError(
86282
- referenceLesson,
86283
- `${referenceLesson.path} is not referenced in any other exercise.`
86284
- );
85996
+ }
85997
+ }
85998
+ const readmeContents = allReadmeContents.join("\n");
85999
+ const referenceLessons = lessons.filter(
86000
+ (lesson) => lesson.sectionName === "reference"
86001
+ );
86002
+ for (const referenceLesson of referenceLessons) {
86003
+ if (!readmeContents.includes(referenceLesson.path)) {
86004
+ errorTracker.addError(
86005
+ referenceLesson,
86006
+ `${referenceLesson.path} is not referenced in any other exercise.`
86007
+ );
86008
+ }
86009
+ }
86010
+ return errorTracker.getErrors();
86011
+ });
86012
+ var lint = Command_exports.make(
86013
+ "lint",
86014
+ {
86015
+ root: rootOption,
86016
+ cwd: cwdOption
86017
+ },
86018
+ Effect_exports.fn("lint")(
86019
+ function* ({ cwd, root }) {
86020
+ const errors = yield* runLint({ cwd, root });
86021
+ const groupedErrors = {};
86022
+ for (const error4 of errors) {
86023
+ if (!groupedErrors[error4.sectionPath]) {
86024
+ groupedErrors[error4.sectionPath] = {};
86025
+ }
86026
+ if (!groupedErrors[error4.sectionPath][error4.lessonPath]) {
86027
+ groupedErrors[error4.sectionPath][error4.lessonPath] = [];
86285
86028
  }
86029
+ groupedErrors[error4.sectionPath][error4.lessonPath].push(error4.error);
86030
+ }
86031
+ for (const [section, exercises] of Object.entries(groupedErrors)) {
86032
+ yield* Console_exports.log(util.styleText(["bold"], section));
86033
+ for (const [exercise2, errs] of Object.entries(exercises)) {
86034
+ yield* Console_exports.log(` ${exercise2}`);
86035
+ for (const err of errs) {
86036
+ yield* Console_exports.log(util.styleText(["red"], ` ${err}`));
86037
+ }
86038
+ }
86039
+ }
86040
+ if (errors.length > 0) {
86041
+ process.exitCode = 1;
86286
86042
  }
86287
- yield* errorTracker.log;
86288
86043
  },
86289
86044
  Effect_exports.catchTags({
86290
86045
  InvalidPathError: (error4) => {
@@ -86307,6 +86062,37 @@ var lint = Command_exports.make(
86307
86062
  "Lint the repository to ensure it is formatted correctly"
86308
86063
  )
86309
86064
  );
86065
+ var runRename = (opts) => Effect_exports.gen(function* () {
86066
+ const lessonParser = yield* LessonParserService;
86067
+ const fs = yield* FileSystem_exports.FileSystem;
86068
+ const lessons = yield* lessonParser.getLessonsFromRepo(opts.root);
86069
+ const sections = /* @__PURE__ */ new Set();
86070
+ for (const lesson of lessons) {
86071
+ sections.add(lesson.sectionPath);
86072
+ }
86073
+ const sectionsAsArray = Array.from(sections).sort(
86074
+ (a, b) => a.localeCompare(b)
86075
+ );
86076
+ let totalLessonsRenamed = 0;
86077
+ for (const section of sectionsAsArray) {
86078
+ const lessonsInSection = lessons.filter((lesson) => lesson.sectionPath === section).sort((a, b) => a.num - b.num);
86079
+ const fullSectionPath = path6__namespace.resolve(opts.root, section);
86080
+ yield* Effect_exports.forEach(lessonsInSection, (lesson, index) => {
86081
+ return Effect_exports.gen(function* () {
86082
+ const newLessonNum = (index + 1).toString().padStart(2, "0");
86083
+ const sectionNum = lesson.sectionNum.toString().padStart(2, "0");
86084
+ const newLessonDirname = `${sectionNum}.${newLessonNum}-${lesson.name}`;
86085
+ const newLessonPath = path6__namespace.join(fullSectionPath, newLessonDirname);
86086
+ if (newLessonPath === lesson.absolutePath()) {
86087
+ return;
86088
+ }
86089
+ yield* fs.rename(lesson.absolutePath(), newLessonPath);
86090
+ totalLessonsRenamed++;
86091
+ });
86092
+ });
86093
+ }
86094
+ return totalLessonsRenamed;
86095
+ });
86310
86096
  var rename4 = Command_exports.make(
86311
86097
  "rename",
86312
86098
  {
@@ -86314,48 +86100,8 @@ var rename4 = Command_exports.make(
86314
86100
  },
86315
86101
  Effect_exports.fn("rename")(
86316
86102
  function* ({ root }) {
86317
- const lessonParser = yield* LessonParserService;
86318
- const fs = yield* FileSystem_exports.FileSystem;
86319
- const lessons = yield* lessonParser.getLessonsFromRepo(
86320
- root
86321
- );
86322
- const sections = /* @__PURE__ */ new Set();
86323
- for (const lesson of lessons) {
86324
- sections.add(lesson.sectionPath);
86325
- }
86326
- const sectionsAsArray = Array.from(sections).sort(
86327
- (a, b) => a.localeCompare(b)
86328
- );
86329
- let totalLessonsRenamed = 0;
86330
- for (const section of sectionsAsArray) {
86331
- const lessonsInSection = lessons.filter((lesson) => lesson.sectionPath === section).sort((a, b) => a.num - b.num);
86332
- const fullSectionPath = path6__namespace.resolve(root, section);
86333
- yield* Effect_exports.forEach(
86334
- lessonsInSection,
86335
- (lesson, index) => {
86336
- return Effect_exports.gen(function* () {
86337
- const newLessonNum = (index + 1).toString().padStart(2, "0");
86338
- const sectionNum = lesson.sectionNum.toString().padStart(2, "0");
86339
- const newLessonDirname = `${sectionNum}.${newLessonNum}-${lesson.name}`;
86340
- const newLessonPath = path6__namespace.join(
86341
- fullSectionPath,
86342
- newLessonDirname
86343
- );
86344
- if (newLessonPath === lesson.absolutePath()) {
86345
- return;
86346
- }
86347
- yield* fs.rename(
86348
- lesson.absolutePath(),
86349
- newLessonPath
86350
- );
86351
- totalLessonsRenamed++;
86352
- });
86353
- }
86354
- );
86355
- }
86356
- yield* Console_exports.log(
86357
- `Renamed ${totalLessonsRenamed} lessons`
86358
- );
86103
+ const totalLessonsRenamed = yield* runRename({ root });
86104
+ yield* Console_exports.log(`Renamed ${totalLessonsRenamed} lessons`);
86359
86105
  },
86360
86106
  Effect_exports.catchTags({
86361
86107
  InvalidPathError: (error4) => {
@@ -87195,7 +86941,7 @@ var editCommit = Command_exports.make(
87195
86941
  yield* Console_exports.log(
87196
86942
  `Resetting to ${targetSha} (${commitLabel})...`
87197
86943
  );
87198
- const resetResult = yield* gitService.resetHard(targetSha).pipe(
86944
+ const resetResult = yield* gitService.applyAsUnstagedChanges(targetSha).pipe(
87199
86945
  Effect_exports.map(() => ({ failed: false })),
87200
86946
  Effect_exports.catchTag(
87201
86947
  "FailedToResetError",
@@ -87207,11 +86953,6 @@ var editCommit = Command_exports.make(
87207
86953
  process.exitCode = 1;
87208
86954
  return;
87209
86955
  }
87210
- yield* Console_exports.log(
87211
- "Undoing commit and unstaging changes..."
87212
- );
87213
- yield* gitService.resetHead();
87214
- yield* gitService.restoreStaged();
87215
86956
  yield* Console_exports.log(
87216
86957
  "\u2713 Reset complete with unstaged changes"
87217
86958
  );
@@ -87529,7 +87270,7 @@ var applyDemoReset = (commitSha) => Effect_exports.gen(function* () {
87529
87270
  const git = yield* GitService;
87530
87271
  yield* Console_exports.log(`
87531
87272
  Resetting to ${commitSha}...`);
87532
- yield* git.resetHard(commitSha).pipe(
87273
+ yield* git.applyAsUnstagedChanges(commitSha).pipe(
87533
87274
  Effect_exports.catchTag("FailedToResetError", () => {
87534
87275
  process.exitCode = 1;
87535
87276
  return Effect_exports.fail(
@@ -87540,10 +87281,6 @@ Resetting to ${commitSha}...`);
87540
87281
  );
87541
87282
  })
87542
87283
  );
87543
- yield* Console_exports.log("Undoing commit...");
87544
- yield* git.resetHead();
87545
- yield* Console_exports.log("Unstaging changes...");
87546
- yield* git.restoreStaged();
87547
87284
  yield* Console_exports.log(
87548
87285
  "\u2713 Commit applied with unstaged changes\n"
87549
87286
  );
@@ -87955,42 +87692,49 @@ var InvalidBranchOperationError2 = class extends Data_exports.TaggedError(
87955
87692
  "InvalidBranchOperationError"
87956
87693
  ) {
87957
87694
  };
87695
+ var UncommittedChangesError = class extends Data_exports.TaggedError(
87696
+ "UncommittedChangesError"
87697
+ ) {
87698
+ };
87699
+ var runPull = (opts) => Effect_exports.gen(function* () {
87700
+ const git = yield* GitService;
87701
+ yield* git.ensureIsGitRepo();
87702
+ const currentBranch = yield* git.getCurrentBranch();
87703
+ if (currentBranch === "main") {
87704
+ return yield* new InvalidBranchOperationError2({
87705
+ message: "Cannot pull when on main branch. Switch to a working branch first."
87706
+ });
87707
+ }
87708
+ const { hasUncommittedChanges, statusOutput } = yield* git.getUncommittedChanges();
87709
+ if (hasUncommittedChanges) {
87710
+ return yield* new UncommittedChangesError({
87711
+ statusOutput
87712
+ });
87713
+ }
87714
+ yield* git.setUpstreamRemote(opts.upstream);
87715
+ yield* Console_exports.log("Fetching main from upstream...");
87716
+ yield* git.fetch("upstream", "main");
87717
+ yield* Console_exports.log(
87718
+ `Merging upstream/main into ${currentBranch}...`
87719
+ );
87720
+ yield* git.merge("upstream/main");
87721
+ yield* Console_exports.log(
87722
+ `
87723
+ \u2713 Successfully merged upstream/main into ${currentBranch}`
87724
+ );
87725
+ });
87958
87726
  var pull = Command_exports.make(
87959
87727
  "pull",
87960
87728
  {
87961
- cwd: cwdOption
87729
+ cwd: cwdOption,
87730
+ upstream: Options_exports.text("upstream").pipe(
87731
+ Options_exports.withDescription(
87732
+ "Git URL or local path to the upstream exercise repo"
87733
+ )
87734
+ )
87962
87735
  },
87963
- ({ cwd }) => Effect_exports.gen(function* () {
87964
- const git = yield* GitService;
87965
- yield* git.ensureIsGitRepo();
87966
- const currentBranch = yield* git.getCurrentBranch();
87967
- if (currentBranch === "main") {
87968
- return yield* new InvalidBranchOperationError2({
87969
- message: "Cannot pull when on main branch. Switch to a working branch first."
87970
- });
87971
- }
87972
- const { hasUncommittedChanges, statusOutput } = yield* git.getUncommittedChanges();
87973
- if (hasUncommittedChanges) {
87974
- yield* Console_exports.error("You have uncommitted changes:\n");
87975
- yield* Console_exports.error(statusOutput);
87976
- yield* Console_exports.error(
87977
- "\nCommit or stash your changes before pulling:\n git stash\n ai-hero pull\n git stash pop"
87978
- );
87979
- process.exitCode = 1;
87980
- return;
87981
- }
87982
- const { remoteName } = yield* git.detectUpstreamRemote();
87983
- yield* Console_exports.log(`Fetching main from ${remoteName}...`);
87984
- yield* git.fetch(remoteName, "main");
87985
- yield* Console_exports.log(
87986
- `Merging ${remoteName}/main into ${currentBranch}...`
87987
- );
87988
- yield* git.merge(`${remoteName}/main`);
87989
- yield* Console_exports.log(
87990
- `
87991
- \u2713 Successfully merged ${remoteName}/main into ${currentBranch}`
87992
- );
87993
- }).pipe(
87736
+ /* v8 ignore start - CLI error handlers are presentation logic */
87737
+ ({ cwd, upstream }) => runPull({ upstream }).pipe(
87994
87738
  Effect_exports.provideService(
87995
87739
  GitServiceConfig,
87996
87740
  GitServiceConfig.of({
@@ -87998,13 +87742,17 @@ var pull = Command_exports.make(
87998
87742
  })
87999
87743
  ),
88000
87744
  Effect_exports.catchTags({
88001
- NotAGitRepoError: (error4) => {
87745
+ UncommittedChangesError: (error4) => {
88002
87746
  return Effect_exports.gen(function* () {
88003
- yield* Console_exports.error(`Error: ${error4.message}`);
87747
+ yield* Console_exports.error("You have uncommitted changes:\n");
87748
+ yield* Console_exports.error(error4.statusOutput);
87749
+ yield* Console_exports.error(
87750
+ "\nCommit or stash your changes before pulling:\n git stash\n ai-hero pull\n git stash pop"
87751
+ );
88004
87752
  process.exitCode = 1;
88005
87753
  });
88006
87754
  },
88007
- NoUpstreamFoundError: (error4) => {
87755
+ NotAGitRepoError: (error4) => {
88008
87756
  return Effect_exports.gen(function* () {
88009
87757
  yield* Console_exports.error(`Error: ${error4.message}`);
88010
87758
  process.exitCode = 1;
@@ -88038,6 +87786,7 @@ var pull = Command_exports.make(
88038
87786
  });
88039
87787
  })
88040
87788
  )
87789
+ /* v8 ignore stop */
88041
87790
  ).pipe(
88042
87791
  Command_exports.withDescription(
88043
87792
  "Pull latest changes from upstream main"
@@ -88049,20 +87798,16 @@ var InvalidBranchOperationError3 = class extends Data_exports.TaggedError(
88049
87798
  "InvalidBranchOperationError"
88050
87799
  ) {
88051
87800
  };
88052
- var InvalidOptionsError = class extends Data_exports.TaggedError(
88053
- "InvalidOptionsError"
88054
- ) {
88055
- };
88056
87801
  var runReset = ({
88057
87802
  branch,
88058
87803
  demo,
88059
87804
  lessonId,
88060
- problem,
88061
- solution
87805
+ upstream
88062
87806
  }) => Effect_exports.gen(function* () {
88063
87807
  const git = yield* GitService;
88064
87808
  const promptService = yield* PromptService;
88065
87809
  yield* git.ensureIsGitRepo();
87810
+ yield* git.setUpstreamRemote(upstream);
88066
87811
  yield* git.ensureUpstreamBranchConnected({
88067
87812
  targetBranch: branch
88068
87813
  });
@@ -88072,32 +87817,7 @@ var runReset = ({
88072
87817
  promptMessage: "Which lesson do you want to reset to? (type to search)",
88073
87818
  excludeCurrentBranch: false
88074
87819
  });
88075
- let commitToUse = targetCommit.sha;
88076
- let stateDescription = "final code";
88077
- if (problem && solution) {
88078
- return yield* new InvalidOptionsError({
88079
- message: "Cannot use both --problem and --solution flags"
88080
- });
88081
- }
88082
- if (demo && (problem || solution)) {
88083
- return yield* new InvalidOptionsError({
88084
- message: "Cannot use --demo with --problem or --solution flags"
88085
- });
88086
- }
88087
- if (!problem && !solution && !demo) {
88088
- const state = yield* promptService.selectProblemOrSolution();
88089
- if (state === "problem") {
88090
- commitToUse = yield* getParentCommit({
88091
- commitSha: targetCommit.sha
88092
- });
88093
- stateDescription = "problem state";
88094
- }
88095
- } else if (problem) {
88096
- commitToUse = yield* getParentCommit({
88097
- commitSha: targetCommit.sha
88098
- });
88099
- stateDescription = "problem state";
88100
- }
87820
+ const commitToUse = targetCommit.sha;
88101
87821
  const currentBranch = yield* git.getCurrentBranch();
88102
87822
  let action;
88103
87823
  if (currentBranch === "main") {
@@ -88124,7 +87844,7 @@ var runReset = ({
88124
87844
  "new"
88125
87845
  );
88126
87846
  yield* Console_exports.log(
88127
- `Creating branch ${branchName} from ${commitToUse} (${stateDescription})...`
87847
+ `Creating branch ${branchName} from ${selectedLessonId}...`
88128
87848
  );
88129
87849
  yield* git.checkoutNewBranchAt(branchName, commitToUse);
88130
87850
  yield* Console_exports.log(
@@ -88143,21 +87863,17 @@ var runReset = ({
88143
87863
  }
88144
87864
  }
88145
87865
  yield* Console_exports.log(
88146
- `Resetting to ${commitToUse} (${stateDescription})...`
87866
+ `Resetting to ${selectedLessonId}...`
88147
87867
  );
88148
- yield* git.resetHard(commitToUse);
88149
87868
  if (demo) {
87869
+ yield* git.applyAsUnstagedChanges(commitToUse);
88150
87870
  yield* Console_exports.log(
88151
- "Undoing commit and unstaging changes..."
88152
- );
88153
- yield* git.resetHead();
88154
- yield* git.restoreStaged();
88155
- yield* Console_exports.log(
88156
- `\u2713 Demo mode: Reset to lesson ${selectedLessonId} with unstaged changes`
87871
+ `\u2713 Demo mode: Reset to ${selectedLessonId} with unstaged changes`
88157
87872
  );
88158
87873
  } else {
87874
+ yield* git.resetHard(commitToUse);
88159
87875
  yield* Console_exports.log(
88160
- `\u2713 Reset to lesson ${selectedLessonId} (${stateDescription})`
87876
+ `\u2713 Reset to ${selectedLessonId}`
88161
87877
  );
88162
87878
  }
88163
87879
  });
@@ -88173,22 +87889,16 @@ var reset2 = Command_exports.make(
88173
87889
  ),
88174
87890
  Options_exports.withDefault(DEFAULT_PROJECT_TARGET_BRANCH)
88175
87891
  ),
88176
- problem: Options_exports.boolean("problem").pipe(
88177
- Options_exports.withAlias("p"),
88178
- Options_exports.withDescription(
88179
- "Reset to problem state (start the exercise)"
88180
- )
88181
- ),
88182
- solution: Options_exports.boolean("solution").pipe(
88183
- Options_exports.withAlias("s"),
87892
+ demo: Options_exports.boolean("demo").pipe(Options_exports.withAlias("d")),
87893
+ upstream: Options_exports.text("upstream").pipe(
88184
87894
  Options_exports.withDescription(
88185
- "Reset to solution state (final code)"
87895
+ "Git URL or local path to the upstream exercise repo"
88186
87896
  )
88187
87897
  ),
88188
- demo: Options_exports.boolean("demo").pipe(Options_exports.withAlias("d")),
88189
87898
  cwd: cwdOption
88190
87899
  },
88191
- ({ branch, cwd, demo, lessonId, problem, solution }) => runReset({ branch, lessonId, problem, solution, demo }).pipe(
87900
+ /* v8 ignore start - CLI error handlers are presentation logic */
87901
+ ({ branch, cwd, demo, lessonId, upstream }) => runReset({ branch, lessonId, demo, upstream }).pipe(
88192
87902
  Effect_exports.provideService(
88193
87903
  GitServiceConfig,
88194
87904
  GitServiceConfig.of({
@@ -88196,26 +87906,12 @@ var reset2 = Command_exports.make(
88196
87906
  })
88197
87907
  ),
88198
87908
  Effect_exports.catchTags({
88199
- NoParentCommitError: (error4) => {
88200
- return Effect_exports.gen(function* () {
88201
- yield* Console_exports.error(
88202
- `Error: Commit ${error4.commitSha} has no parent commit. Repository may be in an invalid state.`
88203
- );
88204
- process.exitCode = 1;
88205
- });
88206
- },
88207
87909
  NotAGitRepoError: (error4) => {
88208
87910
  return Effect_exports.gen(function* () {
88209
87911
  yield* Console_exports.error(`Error: ${error4.message}`);
88210
87912
  process.exitCode = 1;
88211
87913
  });
88212
87914
  },
88213
- NoUpstreamFoundError: (error4) => {
88214
- return Effect_exports.gen(function* () {
88215
- yield* Console_exports.error(`Error: ${error4.message}`);
88216
- process.exitCode = 1;
88217
- });
88218
- },
88219
87915
  CommitNotFoundError: (error4) => {
88220
87916
  return Effect_exports.gen(function* () {
88221
87917
  yield* Console_exports.error(
@@ -88254,6 +87950,7 @@ var reset2 = Command_exports.make(
88254
87950
  });
88255
87951
  })
88256
87952
  )
87953
+ /* v8 ignore stop */
88257
87954
  ).pipe(
88258
87955
  Command_exports.withDescription("Reset to a specific lesson commit")
88259
87956
  );