@snappedly-tools/shipyard 0.8.0 → 0.9.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 (45) hide show
  1. package/README.md +24 -14
  2. package/dist/MountConfig-K5ILnfht.d.ts +26 -0
  3. package/dist/{MountConfig-BHnKnA4h.d.ts → SandboxProvider-oUAwYlWm.d.ts} +1 -26
  4. package/dist/{chunk-L6PX5QTU.js → chunk-57EEKW3R.js} +57 -55
  5. package/dist/chunk-57EEKW3R.js.map +1 -0
  6. package/dist/{chunk-JI3HDDMS.js → chunk-HXSZM52J.js} +3 -3
  7. package/dist/{chunk-JI3HDDMS.js.map → chunk-HXSZM52J.js.map} +1 -1
  8. package/dist/{chunk-44I2BL6E.js → chunk-JZUBT4WG.js} +20 -4
  9. package/dist/chunk-JZUBT4WG.js.map +1 -0
  10. package/dist/chunk-NQRFVKCU.js +1840 -0
  11. package/dist/chunk-NQRFVKCU.js.map +1 -0
  12. package/dist/chunk-Z5C4LHVP.js +137 -0
  13. package/dist/chunk-Z5C4LHVP.js.map +1 -0
  14. package/dist/createSandbox-DmbnWAZv.d.ts +739 -0
  15. package/dist/index.d.ts +7 -2534
  16. package/dist/index.js +216 -7269
  17. package/dist/index.js.map +1 -1
  18. package/dist/integrations/github.d.ts +52 -0
  19. package/dist/integrations/github.js +1049 -0
  20. package/dist/integrations/github.js.map +1 -0
  21. package/dist/integrations/releases.d.ts +136 -0
  22. package/dist/integrations/releases.js +500 -0
  23. package/dist/integrations/releases.js.map +1 -0
  24. package/dist/main.js +641 -383
  25. package/dist/main.js.map +1 -1
  26. package/dist/publication-BPoy_M9M.d.ts +1200 -0
  27. package/dist/sandboxes/docker.d.ts +2 -1
  28. package/dist/sandboxes/docker.js +2 -2
  29. package/dist/templates/parallel-planner/main.mts +11 -7
  30. package/dist/templates/parallel-planner/setup.sh +1 -0
  31. package/dist/templates/parallel-planner-with-review/main.mts +11 -7
  32. package/dist/templates/parallel-planner-with-review/setup.sh +1 -0
  33. package/dist/templates/sequential-reviewer/main.mts +7 -3
  34. package/dist/templates/sequential-reviewer/setup.sh +1 -0
  35. package/dist/templates/shared/setup.sh +1 -0
  36. package/dist/templates/simple-loop/main.mts +7 -3
  37. package/dist/templates/simple-loop/setup.sh +1 -0
  38. package/dist/workflow/coordinator/migrations/002_workflow_phase_records.sql +11 -0
  39. package/dist/workflow/coordinator/migrations/003_phase_record_schema_version.sql +6 -0
  40. package/dist/workflow.d.ts +472 -0
  41. package/dist/workflow.js +4345 -0
  42. package/dist/workflow.js.map +1 -0
  43. package/package.json +13 -1
  44. package/dist/chunk-44I2BL6E.js.map +0 -1
  45. package/dist/chunk-L6PX5QTU.js.map +0 -1
package/dist/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { CODEX_MODELS, purgeRunLogs, withFriendlyErrors, DEFAULT_LOG_RETENTION_DAYS, resolveEnv } from './chunk-JI3HDDMS.js';
3
- import { buildImage, removeImage, RunnerLifecycleError, removeRepositoryRunner, defaultImageName, validateExistingRepositoryRunner, RUNNER_INSTALL_METADATA, RUNNER_CONTROLLER_LOCK, recoverRepositoryRunner, REPOSITORY_RUNNER_OWNER_ENV, removeOwnedRepositoryRunnerContainers, RUNNER_WORK_DIR } from './chunk-44I2BL6E.js';
4
- import { SANDBOX_REPO_DIR, Display, InitError, ACTIVATION_LABEL, PRODUCT_NAME, CONFIG_DIR, CLI_NAME, ClackDisplay, ConfigDirError, ExecHostError, LOCKS_DIR, RUNNER_DIR, assertProtectedDirectoryIdentities, repositoryRunnerEnvironment } from './chunk-L6PX5QTU.js';
2
+ import { CODEX_MODELS, purgeRunLogs, withFriendlyErrors, DEFAULT_LOG_RETENTION_DAYS, resolveEnv } from './chunk-HXSZM52J.js';
3
+ import { buildImage, RunnerLifecycleError, removeRepositoryRunner, removeImage, defaultImageName, validateExistingRepositoryRunner, RUNNER_INSTALL_METADATA, RUNNER_CONTROLLER_LOCK, lockOwnsProcess, recoverRepositoryRunner, REPOSITORY_RUNNER_OWNER_ENV, removeOwnedRepositoryRunnerContainers, RUNNER_WORK_DIR } from './chunk-JZUBT4WG.js';
4
+ import { SANDBOX_REPO_DIR, Display, InitError, ACTIVATION_LABEL, PRODUCT_NAME, CONFIG_DIR, CLI_NAME, RUNNER_DIR, ClackDisplay, ConfigDirError, ExecHostError, LOCKS_DIR, assertProtectedDirectoryIdentities, repositoryRunnerEnvironment } from './chunk-57EEKW3R.js';
5
5
  import { NodeContext, NodeRuntime, NodeFileSystem } from '@effect/platform-node';
6
6
  import { Effect, Layer, Option, Exit, Cause } from 'effect';
7
7
  import { Options, Command } from '@effect/cli';
@@ -14,7 +14,7 @@ import { FileSystem } from '@effect/platform';
14
14
  import { fileURLToPath } from 'url';
15
15
  import { hostname, homedir } from 'os';
16
16
  import { createHash } from 'crypto';
17
- import { chmod, lstat, realpath, rm, mkdir, writeFile, readFile, access } from 'fs/promises';
17
+ import { chmod, rm, lstat, realpath, mkdir, writeFile, readFile, access } from 'fs/promises';
18
18
  import 'module';
19
19
 
20
20
  var configDirExists = (repoDir) => Effect.gen(function* () {
@@ -101,6 +101,12 @@ var LOCKFILES = [
101
101
  ["yarn.lock", "yarn"],
102
102
  ["package-lock.json", "npm"]
103
103
  ];
104
+ var DEPENDENCY_COMMANDS = {
105
+ npm: { add: "install", remove: "uninstall" },
106
+ pnpm: { add: "add", remove: "remove" },
107
+ yarn: { add: "add", remove: "remove" },
108
+ bun: { add: "add", remove: "remove" }
109
+ };
104
110
  var detectPackageManager = (repoDir) => Effect.gen(function* () {
105
111
  const fs = yield* FileSystem.FileSystem;
106
112
  const pkgPath = join(repoDir, "package.json");
@@ -124,18 +130,8 @@ var detectPackageManager = (repoDir) => Effect.gen(function* () {
124
130
  }
125
131
  return "npm";
126
132
  });
127
- var addDependencyCommand = (packageManager, pkg) => {
128
- switch (packageManager) {
129
- case "pnpm":
130
- return `pnpm add ${pkg}`;
131
- case "yarn":
132
- return `yarn add ${pkg}`;
133
- case "bun":
134
- return `bun add ${pkg}`;
135
- case "npm":
136
- return `npm install ${pkg}`;
137
- }
138
- };
133
+ var addDependencyCommand = (packageManager, pkg) => `${packageManager} ${DEPENDENCY_COMMANDS[packageManager].add} ${pkg}`;
134
+ var removeDependencyCommand = (packageManager, pkg) => `${packageManager} ${DEPENDENCY_COMMANDS[packageManager].remove} ${pkg}`;
139
135
  var hostHasDependency = (repoDir, pkg) => Effect.gen(function* () {
140
136
  const fs = yield* FileSystem.FileSystem;
141
137
  const pkgPath = join(repoDir, "package.json");
@@ -271,11 +267,9 @@ var ISSUE_TRACKER_REGISTRY = [
271
267
  # Required repository permissions: Contents, Issues, and Pull requests (Read and write); Metadata (Read)
272
268
  # Or leave blank and run: GH_TOKEN="$(gh auth token)" npx shipyard run
273
269
  GH_TOKEN=
274
- # GitHub repository (owner/repository)
275
- GH_REPO=`
270
+ `
276
271
  }
277
272
  ];
278
- var listIssueTrackers = () => ISSUE_TRACKER_REGISTRY;
279
273
  var getIssueTracker = (name) => ISSUE_TRACKER_REGISTRY.find((b) => b.name === name);
280
274
  var getAgent = (name) => AGENT_REGISTRY.find((a) => a.name === name);
281
275
  var SANDBOX_PROVIDER_REGISTRY = [
@@ -295,7 +289,7 @@ function getNextStepsLines() {
295
289
  "2. If using a model subscription, sign in. For Codex:",
296
290
  ` codex --config 'cli_auth_credentials_store="file"' login`,
297
291
  " test -f ~/.codex/auth.json",
298
- `3. Start with \`npx ${CLI_NAME} runner start\` (if installed) or \`npx ${CLI_NAME} run\``
292
+ `3. If the runner installed successfully, start it with \`npx ${CLI_NAME} runner start\`; otherwise run \`npx ${CLI_NAME} run\`.`
299
293
  ];
300
294
  }
301
295
  function getTemplatesDir() {
@@ -358,8 +352,8 @@ var rewriteMainTs = (configDir, agent, model, modelExplicit, sandboxProvider, ma
358
352
  }
359
353
  if (agent.name === "codex" && codexAuth === "chatgpt") {
360
354
  content = content.replace(
361
- /\bdocker\(\)/g,
362
- `docker(${CODEX_CHATGPT_AUTH_OPTIONS})`
355
+ "const sandboxAuthOptions = {};",
356
+ `const sandboxAuthOptions = ${CODEX_CHATGPT_AUTH_OPTIONS};`
363
357
  );
364
358
  }
365
359
  content = content.replace(/\bdocker\b/g, sandboxProvider.name);
@@ -564,11 +558,6 @@ var ensureCodexChatGptAuth = (options) => {
564
558
  });
565
559
  }
566
560
  };
567
- var runEffectPromise = async (effect) => {
568
- const exit = await Effect.runPromiseExit(effect);
569
- if (Exit.isFailure(exit)) throw Cause.squash(exit.cause);
570
- return exit.value;
571
- };
572
561
  var REPOSITORY_RUNNER_WORKFLOW_PATH = ".github/workflows/shipyard-wake.yml";
573
562
  var REPOSITORY_RUNNER_WAKE_EXECUTABLE = "shipyard-wake";
574
563
  var REPOSITORY_RUNNER_WORKFLOW = `name: Shipyard wake-up
@@ -1274,31 +1263,58 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1274
1263
  };
1275
1264
 
1276
1265
  // src/InitRepositoryRunner.ts
1277
- var initializeRepositoryRunner = async (options) => {
1278
- let shouldInstall = options.requested;
1279
- if (shouldInstall === void 0) {
1280
- shouldInstall = options.interactive ? await options.confirm({
1281
- message: "Install a foreground repository runner for labelled GitHub issues?",
1282
- initialValue: true
1283
- }) : false;
1284
- }
1285
- if (!shouldInstall) return { status: "declined" };
1286
- try {
1287
- return { status: "installed", result: await options.install() };
1288
- } catch (error) {
1289
- return {
1290
- status: "failed",
1291
- message: error instanceof Error ? error.message : String(error)
1292
- };
1293
- }
1294
- };
1295
1266
  var repositoryRunnerNextSteps = () => [
1296
- `First, commit and push ${REPOSITORY_RUNNER_WORKFLOW_PATH} to the repository's default branch`,
1267
+ `Ensure ${REPOSITORY_RUNNER_WORKFLOW_PATH} is committed and pushed to the repository's default branch`,
1297
1268
  `Keep the foreground controller open with \`npx ${CLI_NAME} runner start\``,
1298
1269
  `Inspect it from another terminal with \`npx ${CLI_NAME} runner status\``,
1299
1270
  `Stop it with \`npx ${CLI_NAME} runner stop\``,
1300
1271
  `Unregister and delete its protected files with \`npx ${CLI_NAME} runner remove\``
1301
1272
  ];
1273
+ var errorText = (error) => {
1274
+ if (error !== null && typeof error === "object" && "stderr" in error) {
1275
+ const stderr = error.stderr;
1276
+ if (typeof stderr === "string" || Buffer.isBuffer(stderr)) {
1277
+ const text = stderr.toString().trim();
1278
+ if (text) return text;
1279
+ }
1280
+ }
1281
+ return error instanceof Error ? error.message : String(error);
1282
+ };
1283
+ var commitAndPushInitSetup = (repoDir) => {
1284
+ const branch = execFileSync("git", ["branch", "--show-current"], {
1285
+ cwd: repoDir,
1286
+ encoding: "utf8"
1287
+ }).trim();
1288
+ if (!branch) {
1289
+ throw new Error("Cannot push the Shipyard setup from a detached HEAD.");
1290
+ }
1291
+ const paths = [CONFIG_DIR];
1292
+ if (existsSync(join(repoDir, REPOSITORY_RUNNER_WORKFLOW_PATH))) {
1293
+ paths.push(REPOSITORY_RUNNER_WORKFLOW_PATH);
1294
+ }
1295
+ execFileSync("git", ["add", "--", ...paths], {
1296
+ cwd: repoDir,
1297
+ stdio: "ignore"
1298
+ });
1299
+ execFileSync(
1300
+ "git",
1301
+ ["commit", "--only", "-m", "Add Shipyard setup", "--", ...paths],
1302
+ { cwd: repoDir, stdio: "pipe" }
1303
+ );
1304
+ const commit = execFileSync("git", ["rev-parse", "--short", "HEAD"], {
1305
+ cwd: repoDir,
1306
+ encoding: "utf8"
1307
+ }).trim();
1308
+ try {
1309
+ execFileSync("git", ["push", "origin", `HEAD:refs/heads/${branch}`], {
1310
+ cwd: repoDir,
1311
+ stdio: "pipe"
1312
+ });
1313
+ return { branch, commit };
1314
+ } catch (error) {
1315
+ return { branch, commit, pushError: errorText(error) };
1316
+ }
1317
+ };
1302
1318
  var execFileAsync2 = promisify(execFile);
1303
1319
  var CONTROLLER_STATE = ".shipyard-state.json";
1304
1320
  var LAST_FAILURE = ".shipyard-last-failure.json";
@@ -1657,7 +1673,6 @@ var stopChildBounded = async (child, pause) => {
1657
1673
  );
1658
1674
  }
1659
1675
  };
1660
- var lockOwnsProcess = async (lock, adapters) => lock !== void 0 && Number.isInteger(lock.pid) && typeof lock.processStartedAt === "string" && lock.processStartedAt.length > 0 && adapters.isProcessRunning(lock.pid) && await adapters.processIdentity(lock.pid) === lock.processStartedAt;
1661
1676
  var bestEffort = async (operation) => {
1662
1677
  try {
1663
1678
  await operation();
@@ -2199,6 +2214,11 @@ var stopRepositoryRunner = async (options, adapters = defaultAdapters2) => {
2199
2214
  }
2200
2215
  return { pid: lock.pid };
2201
2216
  };
2217
+ var runEffectPromise = async (effect) => {
2218
+ const exit = await Effect.runPromiseExit(effect);
2219
+ if (Exit.isFailure(exit)) throw Cause.squash(exit.cause);
2220
+ return exit.value;
2221
+ };
2202
2222
 
2203
2223
  // src/RepositoryRunnerCommands.ts
2204
2224
  var registrationTokenOption = Options.text("registration-token").pipe(
@@ -2367,7 +2387,78 @@ var runnerCommand = Command.make(
2367
2387
  purgeRunnerCommand
2368
2388
  ])
2369
2389
  );
2370
- var VERSION = "0.8.0" ;
2390
+ var VERSION = "0.9.1" ;
2391
+ var SHIPYARD_PACKAGE_NAME = "@snappedly-tools/shipyard";
2392
+ var isMissingPath = (error) => error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
2393
+ var lstatIfPresent = async (path) => {
2394
+ try {
2395
+ return await lstat(path);
2396
+ } catch (error) {
2397
+ if (isMissingPath(error)) return void 0;
2398
+ throw error;
2399
+ }
2400
+ };
2401
+ var inspectShipyardConfigDirectory = async (repoDir) => {
2402
+ const configDir = join(repoDir, CONFIG_DIR);
2403
+ const info = await lstatIfPresent(configDir);
2404
+ if (!info) return false;
2405
+ if (!info.isDirectory() || info.isSymbolicLink()) {
2406
+ throw new Error(
2407
+ `Refusing to uninstall because ${configDir} is not a real directory.`
2408
+ );
2409
+ }
2410
+ return true;
2411
+ };
2412
+ var removeWakeWorkflow = async (repoDir) => {
2413
+ const githubDir = join(repoDir, ".github");
2414
+ const workflowsDir = join(githubDir, "workflows");
2415
+ const workflowPath = join(repoDir, REPOSITORY_RUNNER_WORKFLOW_PATH);
2416
+ const githubInfo = await lstatIfPresent(githubDir);
2417
+ if (!githubInfo) return { removed: false, preserved: false };
2418
+ if (!githubInfo.isDirectory() || githubInfo.isSymbolicLink()) {
2419
+ return { removed: false, preserved: githubInfo.isSymbolicLink() };
2420
+ }
2421
+ const workflowsInfo = await lstatIfPresent(workflowsDir);
2422
+ if (!workflowsInfo) return { removed: false, preserved: false };
2423
+ if (!workflowsInfo.isDirectory() || workflowsInfo.isSymbolicLink()) {
2424
+ return {
2425
+ removed: false,
2426
+ preserved: workflowsInfo.isSymbolicLink()
2427
+ };
2428
+ }
2429
+ const workflowInfo = await lstatIfPresent(workflowPath);
2430
+ if (!workflowInfo) return { removed: false, preserved: false };
2431
+ if (!workflowInfo.isFile() || workflowInfo.isSymbolicLink()) {
2432
+ return { removed: false, preserved: true };
2433
+ }
2434
+ const workflow = await readFile(workflowPath, "utf8");
2435
+ if (workflow !== REPOSITORY_RUNNER_WORKFLOW) {
2436
+ return { removed: false, preserved: true };
2437
+ }
2438
+ await rm(workflowPath);
2439
+ return { removed: true, preserved: false };
2440
+ };
2441
+ var removeShipyardRepositoryFiles = async (options) => {
2442
+ const configDirExists2 = await inspectShipyardConfigDirectory(options.repoDir);
2443
+ if (configDirExists2) {
2444
+ const configDir = join(options.repoDir, CONFIG_DIR);
2445
+ const runnerDir = join(configDir, RUNNER_DIR);
2446
+ if (await lstatIfPresent(runnerDir)) {
2447
+ throw new Error(
2448
+ `Remove the repository runner at ${runnerDir} before removing Shipyard configuration.`
2449
+ );
2450
+ }
2451
+ }
2452
+ const workflow = await removeWakeWorkflow(options.repoDir);
2453
+ if (configDirExists2) {
2454
+ await rm(join(options.repoDir, CONFIG_DIR), { recursive: true });
2455
+ }
2456
+ return {
2457
+ workflowRemoved: workflow.removed,
2458
+ workflowPreserved: workflow.preserved,
2459
+ configDirectoryRemoved: configDirExists2
2460
+ };
2461
+ };
2371
2462
 
2372
2463
  // src/cli.ts
2373
2464
  var imageNameOption = Options.text("image-name").pipe(
@@ -2383,7 +2474,6 @@ var defaultUidBuildArgs = () => {
2383
2474
  if (gid !== void 0) args.AGENT_GID = String(gid);
2384
2475
  return args;
2385
2476
  };
2386
- var requireConfigDir = (cwd) => requireCanonicalConfigDir(cwd);
2387
2477
  var purgeRunLogsBestEffort = (display, repoDir) => Effect.tryPromise({
2388
2478
  try: () => purgeRunLogs({
2389
2479
  repoDir,
@@ -2503,7 +2593,7 @@ var runCommand2 = Command.make(
2503
2593
  ({ imageName: imageNameFlag, sandbox, skipBuild, entrypoint }) => Effect.gen(function* () {
2504
2594
  const d = yield* Display;
2505
2595
  const cwd = process.cwd();
2506
- const configDir = yield* requireConfigDir(cwd);
2596
+ const configDir = yield* requireCanonicalConfigDir(cwd);
2507
2597
  yield* purgeRunLogsBestEffort(d, cwd);
2508
2598
  const resolvedEntrypoint = resolveRunEntrypoint(
2509
2599
  cwd,
@@ -2562,14 +2652,6 @@ var sandboxOption = Options.text("sandbox").pipe(
2562
2652
  Options.withDescription("Sandbox provider to use (docker)"),
2563
2653
  Options.optional
2564
2654
  );
2565
- var issueTrackerOption = Options.text("issue-tracker").pipe(
2566
- Options.withDescription("Issue tracker to use (github-issues)"),
2567
- Options.optional
2568
- );
2569
- var buildImageOption = Options.choice("build-image", ["true", "false"]).pipe(
2570
- Options.withDescription("Whether to build the sandbox image now"),
2571
- Options.optional
2572
- );
2573
2655
  var installTemplateDepsOption = Options.choice("install-template-deps", [
2574
2656
  "true",
2575
2657
  "false"
@@ -2579,14 +2661,18 @@ var installTemplateDepsOption = Options.choice("install-template-deps", [
2579
2661
  ),
2580
2662
  Options.optional
2581
2663
  );
2582
- var installRunnerOption = Options.choice("install-runner", [
2583
- "true",
2584
- "false"
2585
- ]).pipe(
2664
+ var commitSetupOption = Options.choice("commit-setup", ["true", "false"]).pipe(
2586
2665
  Options.withDescription(
2587
- "Whether to install a foreground repository runner after scaffolding"
2588
- ),
2589
- Options.optional
2666
+ "Whether to commit and push .shipyard/ and any generated runner workflow"
2667
+ )
2668
+ ).pipe(Options.optional);
2669
+ var uninstallYesOption = Options.boolean("yes").pipe(
2670
+ Options.withDescription("confirm uninstall without an interactive prompt")
2671
+ );
2672
+ var uninstallForceOption = Options.boolean("force").pipe(
2673
+ Options.withDescription(
2674
+ "remove local runner files if GitHub unregistration fails"
2675
+ )
2590
2676
  );
2591
2677
  var choiceToTriBool = (opt) => opt._tag === "Some" ? Option.some(opt.value === "true") : Option.none();
2592
2678
  var initCommand = Command.make(
@@ -2598,10 +2684,8 @@ var initCommand = Command.make(
2598
2684
  codexAuth: codexAuthOption,
2599
2685
  model: initModelOption,
2600
2686
  sandbox: sandboxOption,
2601
- issueTracker: issueTrackerOption,
2602
- buildImage: buildImageOption,
2603
2687
  installTemplateDeps: installTemplateDepsOption,
2604
- installRunner: installRunnerOption
2688
+ commitSetup: commitSetupOption
2605
2689
  },
2606
2690
  ({
2607
2691
  imageName: imageNameFlag,
@@ -2610,10 +2694,8 @@ var initCommand = Command.make(
2610
2694
  codexAuth: codexAuthFlag,
2611
2695
  model: modelFlag,
2612
2696
  sandbox: sandboxFlag,
2613
- issueTracker: issueTrackerFlag,
2614
- buildImage: buildImageFlag,
2615
2697
  installTemplateDeps: installTemplateDepsFlag,
2616
- installRunner: installRunnerFlag
2698
+ commitSetup: commitSetupFlag
2617
2699
  }) => Effect.gen(function* () {
2618
2700
  const d = yield* Display;
2619
2701
  const cwd = process.cwd();
@@ -2641,355 +2723,530 @@ var initCommand = Command.make(
2641
2723
  );
2642
2724
  }
2643
2725
  }
2644
- if (issueTrackerFlag._tag === "Some") {
2645
- const valid = getIssueTracker(issueTrackerFlag.value);
2646
- if (!valid) {
2647
- const names = listIssueTrackers().map((t) => t.name).join(", ");
2648
- yield* Effect.fail(
2649
- new InitError({
2650
- message: `Unknown issue tracker "${issueTrackerFlag.value}". Available: ${names}`
2651
- })
2726
+ yield* d.progress(
2727
+ "Initializing Shipyard",
2728
+ (report) => Effect.gen(function* () {
2729
+ const progressAt = (current, message) => report({ current, total: 100, message });
2730
+ const progressRange = (start, end, update) => {
2731
+ const total = Math.max(1, update.total);
2732
+ const fraction = Math.min(1, Math.max(0, update.current / total));
2733
+ progressAt(
2734
+ Math.round(start + (end - start) * fraction),
2735
+ update.message
2736
+ );
2737
+ };
2738
+ const whilePrompt = async (prompt) => {
2739
+ report.pause();
2740
+ try {
2741
+ return await prompt();
2742
+ } finally {
2743
+ report.resume();
2744
+ }
2745
+ };
2746
+ const statusWithProgress = (message, severity) => Effect.acquireUseRelease(
2747
+ Effect.sync(() => report.pause()),
2748
+ () => d.status(message, severity),
2749
+ () => Effect.sync(() => report.resume())
2652
2750
  );
2653
- }
2654
- }
2655
- const buildImageChoice = choiceToTriBool(buildImageFlag);
2656
- const installTemplateDepsChoice = choiceToTriBool(
2657
- installTemplateDepsFlag
2658
- );
2659
- const installRunnerChoice = choiceToTriBool(installRunnerFlag);
2660
- const isInteractive = process.stdin.isTTY === true;
2661
- const failIfNonInteractive = (flag) => Effect.fail(
2662
- new InitError({
2663
- message: `${flag} is required in non-interactive mode (no TTY detected).`
2664
- })
2665
- );
2666
- const resolveConfirmFlag = (params) => Effect.gen(function* () {
2667
- if (params.choice._tag === "Some") return params.choice.value;
2668
- if (!isInteractive) {
2669
- yield* failIfNonInteractive(params.flag);
2670
- }
2671
- const confirmed = yield* Effect.promise(
2672
- () => clack2.confirm({
2673
- message: params.promptMessage,
2674
- initialValue: true
2675
- })
2676
- );
2677
- if (clack2.isCancel(confirmed)) {
2678
- yield* Effect.fail(
2679
- new InitError({ message: params.cancelMessage })
2751
+ const installTemplateDepsChoice = choiceToTriBool(
2752
+ installTemplateDepsFlag
2680
2753
  );
2681
- }
2682
- return confirmed === true;
2683
- });
2684
- const agents = listAgents();
2685
- let selectedAgent;
2686
- if (agentFlag._tag === "Some") {
2687
- const entry = getAgent(agentFlag.value);
2688
- if (!entry) {
2689
- const names = agents.map((a) => a.name).join(", ");
2690
- yield* Effect.fail(
2754
+ const commitSetupChoice = choiceToTriBool(commitSetupFlag);
2755
+ const isInteractive = process.stdin.isTTY === true;
2756
+ const failIfNonInteractive = (flag) => Effect.fail(
2691
2757
  new InitError({
2692
- message: `Unknown agent "${agentFlag.value}". Available: ${names}`
2758
+ message: `${flag} is required in non-interactive mode (no TTY detected).`
2693
2759
  })
2694
2760
  );
2695
- }
2696
- selectedAgent = entry;
2697
- } else {
2698
- if (!isInteractive) {
2699
- yield* failIfNonInteractive("--agent");
2700
- }
2701
- const selected = yield* Effect.promise(
2702
- () => clack2.select({
2703
- message: "Select an agent:",
2704
- initialValue: DEFAULT_AGENT_NAME,
2705
- options: agents.map((a) => ({
2706
- value: a.name,
2707
- label: a.label,
2708
- hint: `Default model: ${a.defaultModel}`
2709
- }))
2710
- })
2711
- );
2712
- if (clack2.isCancel(selected)) {
2713
- yield* Effect.fail(
2714
- new InitError({ message: "Agent selection cancelled." })
2715
- );
2716
- }
2717
- selectedAgent = getAgent(selected);
2718
- }
2719
- const selectedCodexAuth = yield* Effect.tryPromise({
2720
- try: () => resolveCodexAuthMode({
2721
- agentName: selectedAgent.name,
2722
- requested: codexAuthFlag._tag === "Some" ? codexAuthFlag.value : void 0,
2723
- interactive: isInteractive,
2724
- select: async () => {
2725
- const selected = await clack2.select({
2726
- message: "How will you authenticate Codex?",
2727
- initialValue: "chatgpt",
2728
- options: [
2729
- {
2730
- value: "chatgpt",
2731
- label: "Sign in with ChatGPT",
2732
- hint: "Use your ChatGPT subscription"
2733
- },
2734
- {
2735
- value: "api-key",
2736
- label: "OpenAI API key",
2737
- hint: "Use API billing"
2761
+ const resolveConfirmFlag = (params) => Effect.gen(function* () {
2762
+ if (params.choice._tag === "Some") return params.choice.value;
2763
+ if (!isInteractive) {
2764
+ yield* failIfNonInteractive(params.flag);
2765
+ }
2766
+ const confirmed = yield* Effect.promise(
2767
+ () => whilePrompt(
2768
+ () => clack2.confirm({
2769
+ message: params.promptMessage,
2770
+ initialValue: true
2771
+ })
2772
+ )
2773
+ );
2774
+ if (clack2.isCancel(confirmed)) {
2775
+ yield* Effect.fail(
2776
+ new InitError({ message: params.cancelMessage })
2777
+ );
2778
+ }
2779
+ return confirmed === true;
2780
+ });
2781
+ const agents = listAgents();
2782
+ let selectedAgent;
2783
+ if (agentFlag._tag === "Some") {
2784
+ const entry = getAgent(agentFlag.value);
2785
+ if (!entry) {
2786
+ const names = agents.map((a) => a.name).join(", ");
2787
+ yield* Effect.fail(
2788
+ new InitError({
2789
+ message: `Unknown agent "${agentFlag.value}". Available: ${names}`
2790
+ })
2791
+ );
2792
+ }
2793
+ selectedAgent = entry;
2794
+ } else {
2795
+ if (!isInteractive) {
2796
+ yield* failIfNonInteractive("--agent");
2797
+ }
2798
+ const selected = yield* Effect.promise(
2799
+ () => whilePrompt(
2800
+ () => clack2.select({
2801
+ message: "Select an agent:",
2802
+ initialValue: DEFAULT_AGENT_NAME,
2803
+ options: agents.map((a) => ({
2804
+ value: a.name,
2805
+ label: a.label,
2806
+ hint: `Default model: ${a.defaultModel}`
2807
+ }))
2808
+ })
2809
+ )
2810
+ );
2811
+ if (clack2.isCancel(selected)) {
2812
+ yield* Effect.fail(
2813
+ new InitError({ message: "Agent selection cancelled." })
2814
+ );
2815
+ }
2816
+ selectedAgent = getAgent(selected);
2817
+ }
2818
+ progressAt(6, `Selected ${selectedAgent.label}`);
2819
+ const selectedCodexAuth = yield* Effect.tryPromise({
2820
+ try: () => resolveCodexAuthMode({
2821
+ agentName: selectedAgent.name,
2822
+ requested: codexAuthFlag._tag === "Some" ? codexAuthFlag.value : void 0,
2823
+ interactive: isInteractive,
2824
+ select: async () => {
2825
+ const selected = await whilePrompt(
2826
+ () => clack2.select({
2827
+ message: "How will you authenticate Codex?",
2828
+ initialValue: "chatgpt",
2829
+ options: [
2830
+ {
2831
+ value: "chatgpt",
2832
+ label: "Sign in with ChatGPT",
2833
+ hint: "Use your ChatGPT subscription"
2834
+ },
2835
+ {
2836
+ value: "api-key",
2837
+ label: "OpenAI API key",
2838
+ hint: "Use API billing"
2839
+ }
2840
+ ]
2841
+ })
2842
+ );
2843
+ return clack2.isCancel(selected) ? void 0 : selected;
2844
+ }
2845
+ }),
2846
+ catch: (error) => error instanceof InitError ? error : new InitError({
2847
+ message: `Codex authentication selection failed: ${String(error)}`
2848
+ })
2849
+ });
2850
+ progressAt(12, "Authentication method selected");
2851
+ const selectedModel = modelFlag._tag === "Some" ? modelFlag.value : selectedAgent.defaultModel;
2852
+ if (selectedCodexAuth === "chatgpt") {
2853
+ yield* Effect.try({
2854
+ try: () => {
2855
+ report.pause();
2856
+ try {
2857
+ ensureCodexChatGptAuth({
2858
+ cwd,
2859
+ interactive: isInteractive
2860
+ });
2861
+ } finally {
2862
+ report.resume();
2738
2863
  }
2739
- ]
2864
+ },
2865
+ catch: (error) => error instanceof InitError ? error : new InitError({
2866
+ message: `Codex authentication setup failed: ${String(error)}`
2867
+ })
2740
2868
  });
2741
- return clack2.isCancel(selected) ? void 0 : selected;
2742
2869
  }
2743
- }),
2744
- catch: (error) => error instanceof InitError ? error : new InitError({
2745
- message: `Codex authentication selection failed: ${String(error)}`
2746
- })
2747
- });
2748
- const selectedModel = modelFlag._tag === "Some" ? modelFlag.value : selectedAgent.defaultModel;
2749
- if (selectedCodexAuth === "chatgpt") {
2750
- yield* Effect.try({
2751
- try: () => ensureCodexChatGptAuth({
2752
- cwd,
2753
- interactive: isInteractive
2754
- }),
2755
- catch: (error) => error instanceof InitError ? error : new InitError({
2756
- message: `Codex authentication setup failed: ${String(error)}`
2757
- })
2758
- });
2759
- }
2760
- const selectedSandboxProvider = getSandboxProvider("docker");
2761
- const issueTrackers = listIssueTrackers();
2762
- let selectedIssueTracker;
2763
- if (issueTrackerFlag._tag === "Some") {
2764
- selectedIssueTracker = getIssueTracker(issueTrackerFlag.value);
2765
- } else {
2766
- if (!isInteractive) {
2767
- yield* failIfNonInteractive("--issue-tracker");
2768
- }
2769
- const selected = yield* Effect.promise(
2770
- () => clack2.select({
2771
- message: "Select an issue tracker:",
2772
- initialValue: "github-issues",
2773
- options: issueTrackers.map((b) => ({
2774
- value: b.name,
2775
- label: b.label
2776
- }))
2777
- })
2778
- );
2779
- if (clack2.isCancel(selected)) {
2780
- yield* Effect.fail(
2781
- new InitError({
2782
- message: "Issue tracker selection cancelled."
2783
- })
2784
- );
2785
- }
2786
- selectedIssueTracker = getIssueTracker(selected);
2787
- }
2788
- let selectedTemplate;
2789
- if (template._tag === "Some") {
2790
- selectedTemplate = template.value;
2791
- } else {
2792
- if (!isInteractive) {
2793
- yield* failIfNonInteractive("--template");
2794
- }
2795
- const selected = yield* Effect.promise(
2796
- () => clack2.select({
2797
- message: "Select a template:",
2798
- initialValue: "simple-loop",
2799
- options: templates.map((tmpl) => ({
2800
- value: tmpl.name,
2801
- label: tmpl.name,
2802
- hint: tmpl.description
2803
- }))
2804
- })
2805
- );
2806
- if (clack2.isCancel(selected)) {
2807
- yield* Effect.fail(
2808
- new InitError({ message: "Template selection cancelled." })
2809
- );
2810
- }
2811
- selectedTemplate = selected;
2812
- }
2813
- if (selectedIssueTracker.name === "github-issues") {
2814
- const failedLabels = [];
2815
- for (const [name, description, color] of [
2816
- [ACTIVATION_LABEL, `Issues for ${PRODUCT_NAME} to work on`, "F9A825"],
2817
- ["bug", "Something is broken", "D73A4A"],
2818
- ["enhancement", "New feature or improvement", "A2EEEF"],
2819
- ["needs-triage", "Maintainer evaluation needed", "FBCA04"],
2820
- ["needs-info", "Waiting for reporter information", "D4C5F9"],
2821
- ["ready-for-agent", "Ready for agent implementation", "0E8A16"],
2822
- ["ready-for-human", "Requires human implementation", "1D76DB"],
2823
- ["wontfix", "Will not be actioned", "FFFFFF"],
2824
- ["shipyard:blocked", "Shipyard work needs intervention", "B60205"],
2825
- ["shipyard:pending", "Shipyard is working on this ticket", "1D76DB"],
2826
- [
2827
- "shipyard:complete",
2828
- "Shipyard work ready for human review",
2829
- "0E8A16"
2830
- ],
2831
- [
2832
- "shipyard:outstanding-tasks",
2833
- "Spec has uncompleted tickets",
2834
- "FBCA04"
2835
- ]
2836
- ]) {
2837
- try {
2838
- execSync(
2839
- `gh label create "${name}" --description "${description}" --color "${color}" --force`,
2840
- { cwd, stdio: "ignore" }
2870
+ progressAt(18, "Authentication setup complete");
2871
+ const selectedSandboxProvider = getSandboxProvider("docker");
2872
+ const selectedIssueTracker = getIssueTracker("github-issues");
2873
+ let selectedTemplate;
2874
+ if (template._tag === "Some") {
2875
+ selectedTemplate = template.value;
2876
+ } else {
2877
+ if (!isInteractive) {
2878
+ yield* failIfNonInteractive("--template");
2879
+ }
2880
+ const selected = yield* Effect.promise(
2881
+ () => whilePrompt(
2882
+ () => clack2.select({
2883
+ message: "Select a template:",
2884
+ initialValue: "simple-loop",
2885
+ options: templates.map((tmpl) => ({
2886
+ value: tmpl.name,
2887
+ label: tmpl.name,
2888
+ hint: tmpl.description
2889
+ }))
2890
+ })
2891
+ )
2841
2892
  );
2842
- } catch {
2843
- failedLabels.push(name);
2893
+ if (clack2.isCancel(selected)) {
2894
+ yield* Effect.fail(
2895
+ new InitError({ message: "Template selection cancelled." })
2896
+ );
2897
+ }
2898
+ selectedTemplate = selected;
2844
2899
  }
2845
- }
2846
- if (failedLabels.length) {
2847
- let connected = Boolean(process.env.GH_REPO);
2848
- try {
2849
- execSync("git remote get-url origin", { cwd, stdio: "ignore" });
2850
- connected = true;
2851
- } catch {
2900
+ progressAt(24, `Selected ${selectedTemplate} template`);
2901
+ const failedLabels = [];
2902
+ const labels = [
2903
+ [
2904
+ ACTIVATION_LABEL,
2905
+ `Issues for ${PRODUCT_NAME} to work on`,
2906
+ "F9A825"
2907
+ ],
2908
+ ["bug", "Something is broken", "D73A4A"],
2909
+ ["enhancement", "New feature or improvement", "A2EEEF"],
2910
+ ["needs-triage", "Maintainer evaluation needed", "FBCA04"],
2911
+ ["needs-info", "Waiting for reporter information", "D4C5F9"],
2912
+ ["ready-for-agent", "Ready for agent implementation", "0E8A16"],
2913
+ ["ready-for-human", "Requires human implementation", "1D76DB"],
2914
+ ["wontfix", "Will not be actioned", "FFFFFF"],
2915
+ ["shipyard:blocked", "Shipyard work needs intervention", "B60205"],
2916
+ [
2917
+ "shipyard:pending",
2918
+ "Shipyard is working on this ticket",
2919
+ "1D76DB"
2920
+ ],
2921
+ [
2922
+ "shipyard:complete",
2923
+ "Shipyard work ready for human review",
2924
+ "0E8A16"
2925
+ ],
2926
+ [
2927
+ "shipyard:outstanding-tasks",
2928
+ "Spec has uncompleted tickets",
2929
+ "FBCA04"
2930
+ ]
2931
+ ];
2932
+ for (const [index, [name, description, color]] of labels.entries()) {
2933
+ try {
2934
+ execSync(
2935
+ `gh label create "${name}" --description "${description}" --color "${color}" --force`,
2936
+ { cwd, stdio: "ignore" }
2937
+ );
2938
+ } catch {
2939
+ failedLabels.push(name);
2940
+ }
2941
+ progressAt(
2942
+ 24 + Math.round((index + 1) / labels.length * 10),
2943
+ "Provisioning GitHub labels"
2944
+ );
2852
2945
  }
2853
- const message = `Could not create GitHub labels: ${failedLabels.join(", ")}. Check GitHub access and rerun init.`;
2854
- if (connected) yield* Effect.fail(new InitError({ message }));
2855
- console.warn(message);
2856
- }
2857
- }
2858
- yield* d.progress(
2859
- `Scaffolding ${CONFIG_DIR}/ config directory...`,
2860
- (report) => scaffold(cwd, {
2861
- agent: selectedAgent,
2862
- model: selectedModel,
2863
- modelExplicit: modelFlag._tag === "Some",
2864
- templateName: selectedTemplate,
2865
- issueTracker: selectedIssueTracker,
2866
- sandboxProvider: selectedSandboxProvider,
2867
- codexAuth: selectedCodexAuth,
2868
- onProgress: report
2869
- }).pipe(
2870
- Effect.mapError(
2871
- (e) => new InitError({
2872
- message: `${e instanceof Error ? e.message : e}`
2946
+ if (failedLabels.length) {
2947
+ let connected = Boolean(process.env.GH_REPO);
2948
+ try {
2949
+ execSync("git remote get-url origin", { cwd, stdio: "ignore" });
2950
+ connected = true;
2951
+ } catch {
2952
+ }
2953
+ const message = `Could not create GitHub labels: ${failedLabels.join(", ")}. Check GitHub access and rerun init.`;
2954
+ if (connected) yield* Effect.fail(new InitError({ message }));
2955
+ report.pause();
2956
+ try {
2957
+ console.warn(message);
2958
+ } finally {
2959
+ report.resume();
2960
+ }
2961
+ }
2962
+ progressAt(34, `Scaffolding ${CONFIG_DIR}/ config directory`);
2963
+ yield* scaffold(cwd, {
2964
+ agent: selectedAgent,
2965
+ model: selectedModel,
2966
+ modelExplicit: modelFlag._tag === "Some",
2967
+ templateName: selectedTemplate,
2968
+ issueTracker: selectedIssueTracker,
2969
+ sandboxProvider: selectedSandboxProvider,
2970
+ codexAuth: selectedCodexAuth,
2971
+ onProgress: (update) => progressRange(34, 56, update)
2972
+ }).pipe(
2973
+ Effect.mapError(
2974
+ (e) => new InitError({
2975
+ message: `${e instanceof Error ? e.message : e}`
2976
+ })
2977
+ )
2978
+ );
2979
+ progressAt(56, "Shipyard configuration generated");
2980
+ const packageManager = yield* detectPackageManager(cwd);
2981
+ progressAt(58, `Detected ${packageManager}`);
2982
+ if (getTemplateDependencies(selectedTemplate).includes("zod")) {
2983
+ const alreadyInstalled = yield* hostHasDependency(cwd, "zod");
2984
+ if (!alreadyInstalled) {
2985
+ const installCmd = addDependencyCommand(packageManager, "zod");
2986
+ const shouldInstall = yield* resolveConfirmFlag({
2987
+ choice: installTemplateDepsChoice,
2988
+ flag: "--install-template-deps",
2989
+ promptMessage: `The ${selectedTemplate} template needs a schema validator. Install zod now (\`${installCmd}\`)?`,
2990
+ cancelMessage: "Install-template-deps selection cancelled."
2991
+ });
2992
+ progressAt(
2993
+ 60,
2994
+ shouldInstall ? "Installing template dependency" : "Skipping zod installation"
2995
+ );
2996
+ if (shouldInstall) {
2997
+ progressAt(62, `Installing zod with ${packageManager}`);
2998
+ const installed = yield* Effect.sync(() => {
2999
+ try {
3000
+ execSync(installCmd, { cwd, stdio: "ignore" });
3001
+ return true;
3002
+ } catch {
3003
+ return false;
3004
+ }
3005
+ });
3006
+ progressAt(64, "Template dependency setup complete");
3007
+ yield* installed ? statusWithProgress(
3008
+ `Installed zod with ${packageManager}.`,
3009
+ "success"
3010
+ ) : statusWithProgress(
3011
+ `Couldn't install zod automatically. Run \`${installCmd}\` before running the agent.`,
3012
+ "warn"
3013
+ );
3014
+ }
3015
+ }
3016
+ }
3017
+ progressAt(66, "Template dependencies ready");
3018
+ const providerLabel = selectedSandboxProvider.label;
3019
+ const containerfileDir = join(cwd, CONFIG_DIR);
3020
+ progressAt(68, `Building ${providerLabel} image '${imageName}'`);
3021
+ yield* buildImage(imageName, containerfileDir, {
3022
+ buildArgs: defaultUidBuildArgs()
3023
+ });
3024
+ progressAt(78, "Docker image built");
3025
+ yield* statusWithProgress("Image built successfully.", "success");
3026
+ progressAt(78, "Installing repository runner");
3027
+ const runner = yield* Effect.tryPromise({
3028
+ try: () => installRepositoryRunnerWithReplacement({
3029
+ repoDir: cwd,
3030
+ interactive: isInteractive,
3031
+ confirmReplacement: async (conflict) => {
3032
+ const confirmed = await whilePrompt(
3033
+ () => clack2.confirm({
3034
+ message: conflict.confirmationMessage,
3035
+ initialValue: false
3036
+ })
3037
+ );
3038
+ return !clack2.isCancel(confirmed) && confirmed === true;
3039
+ },
3040
+ install: () => installRepositoryRunner({
3041
+ repoDir: cwd,
3042
+ onProgress: (update) => progressRange(78, 94, update)
3043
+ })
3044
+ }),
3045
+ catch: (error) => error instanceof InitError ? error : new InitError({
3046
+ message: `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}. Init is incomplete. Fix the issue and retry with \`npx ${CLI_NAME} runner install\`.`
2873
3047
  })
2874
- )
2875
- )
2876
- );
2877
- const packageManager = yield* detectPackageManager(cwd);
2878
- if (getTemplateDependencies(selectedTemplate).includes("zod")) {
2879
- const alreadyInstalled = yield* hostHasDependency(cwd, "zod");
2880
- if (!alreadyInstalled) {
2881
- const installCmd = addDependencyCommand(packageManager, "zod");
2882
- const shouldInstall = yield* resolveConfirmFlag({
2883
- choice: installTemplateDepsChoice,
2884
- flag: "--install-template-deps",
2885
- promptMessage: `The ${selectedTemplate} template needs a schema validator. Install zod now (\`${installCmd}\`)?`,
2886
- cancelMessage: "Install-template-deps selection cancelled."
2887
3048
  });
2888
- if (shouldInstall) {
2889
- const installed = yield* Effect.sync(() => {
3049
+ progressAt(94, "Repository runner installed");
3050
+ yield* statusWithProgress(
3051
+ `Installed ${runner.name} for ${runner.repository}.`,
3052
+ "success"
3053
+ );
3054
+ let shouldCommitSetup = commitSetupChoice._tag === "Some" && commitSetupChoice.value;
3055
+ if (commitSetupChoice._tag === "None" && isInteractive) {
3056
+ const confirmed = yield* Effect.promise(
3057
+ () => whilePrompt(
3058
+ () => clack2.confirm({
3059
+ message: "Commit .shipyard/ and any generated runner workflow, then push this branch to origin? Sandboxes need the setup committed. The push also sends any local commits not already on origin.",
3060
+ initialValue: true
3061
+ })
3062
+ )
3063
+ );
3064
+ if (clack2.isCancel(confirmed)) {
3065
+ yield* Effect.fail(
3066
+ new InitError({
3067
+ message: "Shipyard setup commit cancelled."
3068
+ })
3069
+ );
3070
+ }
3071
+ shouldCommitSetup = confirmed === true;
3072
+ }
3073
+ progressAt(
3074
+ 96,
3075
+ shouldCommitSetup ? "Committing and pushing Shipyard setup" : "Setup commit skipped"
3076
+ );
3077
+ if (shouldCommitSetup) {
3078
+ const publishResult = yield* Effect.sync(() => {
2890
3079
  try {
2891
- execSync(installCmd, { cwd, stdio: "ignore" });
2892
- return true;
2893
- } catch {
2894
- return false;
3080
+ return {
3081
+ status: "committed",
3082
+ result: commitAndPushInitSetup(cwd)
3083
+ };
3084
+ } catch (error) {
3085
+ return {
3086
+ status: "failed",
3087
+ message: error instanceof Error ? error.message : String(error)
3088
+ };
2895
3089
  }
2896
3090
  });
2897
- yield* installed ? d.status(`Installed zod with ${packageManager}.`, "success") : d.status(
2898
- `Couldn't install zod automatically. Run \`${installCmd}\` before running the agent.`,
3091
+ progressAt(99, "Shipyard setup commit finished");
3092
+ if (publishResult.status === "failed") {
3093
+ yield* statusWithProgress(
3094
+ `Could not commit the Shipyard setup: ${publishResult.message}`,
3095
+ "warn"
3096
+ );
3097
+ yield* statusWithProgress(
3098
+ `Commit \`${CONFIG_DIR}/\` before running Shipyard.`,
3099
+ "warn"
3100
+ );
3101
+ } else if (publishResult.result.pushError) {
3102
+ yield* statusWithProgress(
3103
+ `Committed ${publishResult.result.commit}, but could not push to origin/${publishResult.result.branch}: ${publishResult.result.pushError}`,
3104
+ "warn"
3105
+ );
3106
+ yield* statusWithProgress(
3107
+ `Push it later with \`git push origin ${publishResult.result.branch}\`.`,
3108
+ "warn"
3109
+ );
3110
+ } else {
3111
+ yield* statusWithProgress(
3112
+ `Committed ${publishResult.result.commit} and pushed origin/${publishResult.result.branch}.`,
3113
+ "success"
3114
+ );
3115
+ }
3116
+ } else {
3117
+ yield* statusWithProgress(
3118
+ `Commit \`${CONFIG_DIR}/\` before running Shipyard. Sandboxes do not receive uncommitted setup files.`,
3119
+ "warn"
3120
+ );
3121
+ yield* statusWithProgress(
3122
+ "Push the runner wake workflow to the repository's default branch before starting the runner.",
2899
3123
  "warn"
2900
3124
  );
2901
3125
  }
2902
- }
3126
+ progressAt(100, "Initialization complete");
3127
+ })
3128
+ );
3129
+ yield* d.text("Repository runner next steps:");
3130
+ for (const [index, line] of repositoryRunnerNextSteps().entries()) {
3131
+ yield* d.text(styleText("dim", `${index + 1}. ${line}`));
3132
+ }
3133
+ yield* d.status("Init complete!", "success");
3134
+ const nextSteps = getNextStepsLines();
3135
+ for (const [i, line] of nextSteps.entries()) {
3136
+ yield* d.text(i === 0 ? line : styleText("dim", line));
2903
3137
  }
2904
- const providerLabel = selectedSandboxProvider.label;
2905
- const shouldBuild = yield* resolveConfirmFlag({
2906
- choice: buildImageChoice,
2907
- flag: "--build-image",
2908
- promptMessage: `Build the default ${providerLabel} image now?`,
2909
- cancelMessage: "Build-image selection cancelled."
3138
+ })
3139
+ );
3140
+ var uninstallCommand = Command.make(
3141
+ "uninstall",
3142
+ { yes: uninstallYesOption, force: uninstallForceOption },
3143
+ ({ yes, force }) => Effect.gen(function* () {
3144
+ const d = yield* Display;
3145
+ const repoDir = process.cwd();
3146
+ const configDirExists2 = yield* Effect.tryPromise({
3147
+ try: () => inspectShipyardConfigDirectory(repoDir),
3148
+ catch: (error) => new InitError({
3149
+ message: `Could not inspect Shipyard configuration: ${error instanceof Error ? error.message : String(error)}`
3150
+ })
2910
3151
  });
2911
- if (shouldBuild) {
2912
- const containerfileDir = join(cwd, CONFIG_DIR);
2913
- yield* d.spinner(
2914
- `Building ${providerLabel} image '${imageName}'...`,
2915
- buildImage(imageName, containerfileDir, {
2916
- buildArgs: defaultUidBuildArgs()
2917
- })
2918
- );
2919
- yield* d.status("Image built successfully.", "success");
2920
- } else {
3152
+ const runnerDir = join(repoDir, CONFIG_DIR, RUNNER_DIR);
3153
+ const runnerInstalled = configDirExists2 && existsSync(runnerDir);
3154
+ const workflowExists = existsSync(
3155
+ join(repoDir, REPOSITORY_RUNNER_WORKFLOW_PATH)
3156
+ );
3157
+ const packageInstalled = yield* hostHasDependency(
3158
+ repoDir,
3159
+ SHIPYARD_PACKAGE_NAME
3160
+ );
3161
+ if (!configDirExists2 && !workflowExists && !runnerInstalled && !packageInstalled) {
2921
3162
  yield* d.status(
2922
- `Run \`${CLI_NAME} ${selectedSandboxProvider.cliNamespace} build-image\` to build the ${providerLabel} image later.`,
3163
+ "No Shipyard installation found in this repository.",
2923
3164
  "info"
2924
3165
  );
3166
+ return;
2925
3167
  }
2926
- const runnerInit = yield* Effect.tryPromise({
2927
- try: () => initializeRepositoryRunner({
2928
- interactive: isInteractive,
2929
- requested: installRunnerChoice._tag === "Some" ? installRunnerChoice.value : void 0,
2930
- confirm: async ({ message, initialValue }) => {
2931
- const confirmed = await clack2.confirm({ message, initialValue });
2932
- if (clack2.isCancel(confirmed)) {
2933
- throw new InitError({
2934
- message: "Repository-runner installation selection cancelled."
2935
- });
2936
- }
2937
- return confirmed === true;
2938
- },
2939
- install: () => installRepositoryRunnerWithReplacement({
2940
- repoDir: cwd,
2941
- interactive: isInteractive,
2942
- confirmReplacement: async (conflict) => {
2943
- const confirmed = await clack2.confirm({
2944
- message: conflict.confirmationMessage,
2945
- initialValue: false
2946
- });
2947
- return !clack2.isCancel(confirmed) && confirmed === true;
2948
- },
2949
- install: () => runEffectPromise(
2950
- d.progress(
2951
- "Installing repository runner",
2952
- (report) => Effect.tryPromise({
2953
- try: () => installRepositoryRunner({
2954
- repoDir: cwd,
2955
- onProgress: report
2956
- }),
2957
- catch: (error) => error instanceof RunnerInstallError ? error : new InitError({
2958
- message: `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
2959
- })
2960
- })
2961
- )
2962
- )
3168
+ if (!yes && process.stdin.isTTY !== true) {
3169
+ return yield* Effect.fail(
3170
+ new InitError({
3171
+ message: "Shipyard uninstall needs confirmation. Run it in a terminal or pass --yes."
2963
3172
  })
2964
- }),
2965
- catch: (error) => error instanceof InitError ? error : new InitError({
2966
- message: `Repository-runner setup failed: ${error instanceof Error ? error.message : String(error)}`
3173
+ );
3174
+ }
3175
+ if (!yes) {
3176
+ const actions = [
3177
+ runnerInstalled ? "unregister and remove the repository runner" : null,
3178
+ configDirExists2 ? `remove all of ${CONFIG_DIR}/, including .env and runtime data` : null,
3179
+ workflowExists ? "remove the generated runner wake workflow" : null,
3180
+ packageInstalled ? `remove ${SHIPYARD_PACKAGE_NAME} from package.json` : null
3181
+ ].filter((action) => action !== null);
3182
+ const confirmation = yield* Effect.tryPromise({
3183
+ try: () => clack2.confirm({
3184
+ message: `Uninstall Shipyard from ${repoDir}? This will ${actions.join(", ")}. It leaves GitHub issues and labels unchanged.`,
3185
+ initialValue: false
3186
+ }),
3187
+ catch: (error) => new InitError({
3188
+ message: `Could not confirm Shipyard uninstall: ${error instanceof Error ? error.message : String(error)}`
3189
+ })
3190
+ });
3191
+ if (clack2.isCancel(confirmation) || confirmation !== true) {
3192
+ yield* d.status("Shipyard uninstall cancelled.", "info");
3193
+ return;
3194
+ }
3195
+ }
3196
+ if (runnerInstalled) {
3197
+ const result = yield* Effect.tryPromise({
3198
+ try: () => removeRepositoryRunner({ repoDir, force }),
3199
+ catch: (error) => error instanceof RunnerLifecycleError ? new InitError({ message: error.message }) : new InitError({
3200
+ message: `Repository runner removal failed: ${error instanceof Error ? error.message : String(error)}`
3201
+ })
3202
+ });
3203
+ yield* d.status("Repository runner removed.", "success");
3204
+ if (result.manualCleanup) {
3205
+ yield* d.status(result.manualCleanup, "warn");
3206
+ }
3207
+ }
3208
+ const files = yield* Effect.tryPromise({
3209
+ try: () => removeShipyardRepositoryFiles({ repoDir }),
3210
+ catch: (error) => new InitError({
3211
+ message: `Could not remove Shipyard repository files: ${error instanceof Error ? error.message : String(error)}`
2967
3212
  })
2968
3213
  });
2969
- if (runnerInit.status === "installed") {
3214
+ if (files.configDirectoryRemoved) {
3215
+ yield* d.status(`Removed all of ${CONFIG_DIR}/.`, "success");
3216
+ }
3217
+ if (files.workflowRemoved) {
2970
3218
  yield* d.status(
2971
- `Installed ${runnerInit.result.name} for ${runnerInit.result.repository}.`,
3219
+ `Removed ${REPOSITORY_RUNNER_WORKFLOW_PATH}.`,
2972
3220
  "success"
2973
3221
  );
2974
- yield* d.text("Repository runner next steps:");
2975
- for (const [index, line] of repositoryRunnerNextSteps().entries()) {
2976
- yield* d.text(styleText("dim", `${index + 1}. ${line}`));
2977
- }
2978
- } else if (runnerInit.status === "failed") {
2979
3222
  yield* d.status(
2980
- `Shipyard scaffolding is ready, but repository runner installation failed: ${runnerInit.message}`,
2981
- "warn"
3223
+ "Commit and push the workflow deletion to disable it on GitHub.",
3224
+ "info"
2982
3225
  );
3226
+ } else if (files.workflowPreserved) {
2983
3227
  yield* d.status(
2984
- `Retry from this repository with \`npx ${CLI_NAME} runner install\`.`,
3228
+ `Preserved ${REPOSITORY_RUNNER_WORKFLOW_PATH} because it is customized or linked.`,
2985
3229
  "warn"
2986
3230
  );
2987
3231
  }
2988
- yield* d.status("Init complete!", "success");
2989
- const nextSteps = getNextStepsLines();
2990
- for (const [i, line] of nextSteps.entries()) {
2991
- yield* d.text(i === 0 ? line : styleText("dim", line));
3232
+ if (packageInstalled) {
3233
+ const packageManager = yield* detectPackageManager(repoDir);
3234
+ const command = removeDependencyCommand(
3235
+ packageManager,
3236
+ SHIPYARD_PACKAGE_NAME
3237
+ );
3238
+ yield* Effect.try({
3239
+ try: () => execSync(command, { cwd: repoDir, stdio: "inherit" }),
3240
+ catch: (error) => new InitError({
3241
+ message: `Could not remove ${SHIPYARD_PACKAGE_NAME}. Rerun the uninstall after resolving the package-manager error: ${error instanceof Error ? error.message : String(error)}`
3242
+ })
3243
+ });
3244
+ yield* d.status(
3245
+ `Removed ${SHIPYARD_PACKAGE_NAME} with ${packageManager}.`,
3246
+ "success"
3247
+ );
2992
3248
  }
3249
+ yield* d.status("Shipyard uninstalled from this repository.", "success");
2993
3250
  })
2994
3251
  );
2995
3252
  var dockerfileOption = Options.file("dockerfile").pipe(
@@ -3007,7 +3264,7 @@ var buildImageCommand = Command.make(
3007
3264
  ({ imageName: imageNameFlag, dockerfile }) => Effect.gen(function* () {
3008
3265
  const d = yield* Display;
3009
3266
  const cwd = process.cwd();
3010
- yield* requireConfigDir(cwd);
3267
+ yield* requireCanonicalConfigDir(cwd);
3011
3268
  const imageName = resolveImageName(imageNameFlag, cwd);
3012
3269
  const dockerfileDir = join(cwd, CONFIG_DIR);
3013
3270
  const dockerfilePath = dockerfile._tag === "Some" ? dockerfile.value : void 0;
@@ -3060,6 +3317,7 @@ var rootCommand = Command.make(
3060
3317
  var shipyard = rootCommand.pipe(
3061
3318
  Command.withSubcommands([
3062
3319
  initCommand,
3320
+ uninstallCommand,
3063
3321
  runCommand2,
3064
3322
  dockerCommand,
3065
3323
  runnerCommand