@snappedly-tools/shipyard 0.7.0 → 0.8.0

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 (41) hide show
  1. package/README.md +34 -41
  2. package/dist/MountConfig-BHnKnA4h.d.ts +145 -0
  3. package/dist/{chunk-WAXEMZUV.js → chunk-44I2BL6E.js} +225 -33
  4. package/dist/chunk-44I2BL6E.js.map +1 -0
  5. package/dist/{chunk-56TDSWFU.js → chunk-JI3HDDMS.js} +3 -3
  6. package/dist/{chunk-56TDSWFU.js.map → chunk-JI3HDDMS.js.map} +1 -1
  7. package/dist/{chunk-FDYOTN55.js → chunk-L6PX5QTU.js} +253 -878
  8. package/dist/chunk-L6PX5QTU.js.map +1 -0
  9. package/dist/index.d.ts +117 -97
  10. package/dist/index.js +322 -344
  11. package/dist/index.js.map +1 -1
  12. package/dist/main.js +235 -139
  13. package/dist/main.js.map +1 -1
  14. package/dist/sandboxes/docker.d.ts +1 -3
  15. package/dist/sandboxes/docker.js +2 -4
  16. package/dist/templates/parallel-planner/main.mts +67 -9
  17. package/dist/templates/parallel-planner/planner-branch.mts +151 -0
  18. package/dist/templates/parallel-planner-with-review/main.mts +70 -12
  19. package/dist/templates/parallel-planner-with-review/planner-branch.mts +151 -0
  20. package/dist/templates/sequential-reviewer/main.mts +52 -3
  21. package/dist/templates/simple-loop/main.mts +51 -2
  22. package/package.json +1 -17
  23. package/dist/MountConfig-bZoCs4Dd.d.ts +0 -26
  24. package/dist/SandboxProvider-XJQqEdSf.d.ts +0 -261
  25. package/dist/chunk-ACD46ZM4.js +0 -136
  26. package/dist/chunk-ACD46ZM4.js.map +0 -1
  27. package/dist/chunk-FDYOTN55.js.map +0 -1
  28. package/dist/chunk-KMGNFXKN.js +0 -38
  29. package/dist/chunk-KMGNFXKN.js.map +0 -1
  30. package/dist/chunk-SOJTAJTF.js +0 -78
  31. package/dist/chunk-SOJTAJTF.js.map +0 -1
  32. package/dist/chunk-WAXEMZUV.js.map +0 -1
  33. package/dist/sandboxes/no-sandbox.d.ts +0 -37
  34. package/dist/sandboxes/no-sandbox.js +0 -4
  35. package/dist/sandboxes/no-sandbox.js.map +0 -1
  36. package/dist/sandboxes/vercel.d.ts +0 -104
  37. package/dist/sandboxes/vercel.js +0 -166
  38. package/dist/sandboxes/vercel.js.map +0 -1
  39. package/dist/templates/blank/main.mts +0 -13
  40. package/dist/templates/blank/prompt.md +0 -12
  41. package/dist/templates/blank/template.json +0 -4
package/dist/main.js CHANGED
@@ -1,13 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { CODEX_MODELS, purgeRunLogs, withFriendlyErrors, DEFAULT_LOG_RETENTION_DAYS, resolveEnv } from './chunk-56TDSWFU.js';
3
- import { buildImage, removeImage, validateExistingRepositoryRunner, RunnerLifecycleError, RUNNER_INSTALL_METADATA, removeRepositoryRunner, RUNNER_CONTROLLER_LOCK, recoverRepositoryRunner, REPOSITORY_RUNNER_OWNER_ENV, removeOwnedRepositoryRunnerContainers, RUNNER_WORK_DIR } from './chunk-WAXEMZUV.js';
4
- import { SANDBOX_REPO_DIR, Display, InitError, ACTIVATION_LABEL, PRODUCT_NAME, CONFIG_DIR, CLI_NAME, ClackDisplay, RUNNER_DIR, RUNNER_SANDBOX_MASK_DIR, assertProtectedDirectoryIdentities, repositoryRunnerEnvironment, ConfigDirError, defaultImageName, ExecHostError, LOCKS_DIR } from './chunk-FDYOTN55.js';
5
- import './chunk-KMGNFXKN.js';
6
- import './chunk-ACD46ZM4.js';
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';
7
5
  import { NodeContext, NodeRuntime, NodeFileSystem } from '@effect/platform-node';
8
- import { Effect, Layer, Option } from 'effect';
6
+ import { Effect, Layer, Option, Exit, Cause } from 'effect';
9
7
  import { Options, Command } from '@effect/cli';
10
- import * as clack from '@clack/prompts';
8
+ import * as clack2 from '@clack/prompts';
11
9
  import { execFile, execSync, spawn, execFileSync } from 'child_process';
12
10
  import { existsSync, constants, statSync } from 'fs';
13
11
  import { join, resolve, dirname } from 'path';
@@ -19,18 +17,12 @@ import { createHash } from 'crypto';
19
17
  import { chmod, lstat, realpath, rm, mkdir, writeFile, readFile, access } from 'fs/promises';
20
18
  import 'module';
21
19
 
22
- var pathExists = (path) => Effect.gen(function* () {
20
+ var configDirExists = (repoDir) => Effect.gen(function* () {
23
21
  const fs = yield* FileSystem.FileSystem;
24
- return yield* fs.exists(path).pipe(Effect.catchAll(() => Effect.succeed(false)));
25
- });
26
- var getConfigDirState = (repoDir) => Effect.gen(function* () {
27
- const canonicalExists = yield* pathExists(join(repoDir, CONFIG_DIR));
28
- if (canonicalExists) return "canonical";
29
- return "missing";
22
+ return yield* fs.exists(join(repoDir, CONFIG_DIR)).pipe(Effect.catchAll(() => Effect.succeed(false)));
30
23
  });
31
24
  var requireCanonicalConfigDir = (repoDir) => Effect.gen(function* () {
32
- const state = yield* getConfigDirState(repoDir);
33
- if (state === "missing") {
25
+ if (!(yield* configDirExists(repoDir))) {
34
26
  return yield* Effect.fail(
35
27
  new ConfigDirError({
36
28
  message: `No ${CONFIG_DIR}/ found. Run \`${CLI_NAME} init\` first.`
@@ -40,8 +32,7 @@ var requireCanonicalConfigDir = (repoDir) => Effect.gen(function* () {
40
32
  return join(repoDir, CONFIG_DIR);
41
33
  });
42
34
  var assertConfigDirAvailable = (repoDir) => Effect.gen(function* () {
43
- const state = yield* getConfigDirState(repoDir);
44
- if (state === "canonical") {
35
+ if (yield* configDirExists(repoDir)) {
45
36
  return yield* Effect.fail(
46
37
  new ConfigDirError({
47
38
  message: `${CONFIG_DIR}/ directory already exists. ${CLI_NAME} did not modify it. Use the existing configuration or remove it deliberately after preserving any active worktrees before re-initializing.`
@@ -71,11 +62,16 @@ var CODEX_CHATGPT_AUTH_OPTIONS = `{
71
62
  },
72
63
  ],
73
64
  }`;
65
+ var roleModelEnvExample = (agentName) => agentName === "codex" ? `# Optional model choices for Shipyard workflows.
66
+ # Choose models available to your Codex CLI account: https://learn.chatgpt.com/docs/models
67
+ # The provider must support each value. Aliases can change their target over time.
68
+ # SHIPYARD_ROUTINE_MODEL=gpt-6-luna
69
+ # SHIPYARD_STRONG_MODEL=gpt-6-sol` : `# Optional model choices for Shipyard workflows.
70
+ # Choose aliases or model IDs available to your Claude Code provider: https://code.claude.com/docs/en/model-config
71
+ # The provider must support each value. Aliases can change their target over time.
72
+ # SHIPYARD_ROUTINE_MODEL=sonnet
73
+ # SHIPYARD_STRONG_MODEL=opus`;
74
74
  var TEMPLATES = [
75
- {
76
- name: "blank",
77
- description: "Bare scaffold \u2014 write your own prompt and orchestration"
78
- },
79
75
  {
80
76
  name: "simple-loop",
81
77
  description: "Implements standalone issues one by one and opens review PRs"
@@ -175,7 +171,7 @@ RUN apt-get update && apt-get install -y \\
175
171
 
176
172
  # Build-args for UID/GID alignment: shipyard docker build-image
177
173
  # defaults these to the host user's UID/GID so image-built files
178
- # and bind-mounted files share an owner without runtime chown.
174
+ # and explicit mounted files share an owner without runtime chown.
179
175
  ARG AGENT_UID=1000
180
176
  ARG AGENT_GID=1000
181
177
 
@@ -191,8 +187,8 @@ ENV PATH="/home/agent/.local/bin:$PATH"
191
187
 
192
188
  WORKDIR /home/agent
193
189
 
194
- # In worktree sandbox mode, Shipyard bind-mounts the git worktree at ${SANDBOX_REPO_DIR}
195
- # and overrides the working directory to ${SANDBOX_REPO_DIR} at container start.
190
+ # Shipyard syncs Git history into a sandbox-owned repository at ${SANDBOX_REPO_DIR}
191
+ # and runs agent commands from ${SANDBOX_REPO_DIR}.
196
192
  # Structure your Dockerfile so that ${SANDBOX_REPO_DIR} can serve as the project root.
197
193
  ENTRYPOINT ["sleep", "infinity"]
198
194
  `;
@@ -209,7 +205,7 @@ RUN apt-get update && apt-get install -y \\
209
205
 
210
206
  # Build-args for UID/GID alignment: shipyard docker build-image
211
207
  # defaults these to the host user's UID/GID so image-built files
212
- # and bind-mounted files share an owner without runtime chown.
208
+ # and explicit mounted files share an owner without runtime chown.
213
209
  ARG AGENT_UID=1000
214
210
  ARG AGENT_GID=1000
215
211
 
@@ -223,8 +219,8 @@ USER \${AGENT_UID}:\${AGENT_GID}
223
219
 
224
220
  WORKDIR /home/agent
225
221
 
226
- # In worktree sandbox mode, Shipyard bind-mounts the git worktree at ${SANDBOX_REPO_DIR}
227
- # and overrides the working directory to ${SANDBOX_REPO_DIR} at container start.
222
+ # Shipyard syncs Git history into a sandbox-owned repository at ${SANDBOX_REPO_DIR}
223
+ # and runs agent commands from ${SANDBOX_REPO_DIR}.
228
224
  # Structure your Dockerfile so that ${SANDBOX_REPO_DIR} can serve as the project root.
229
225
  ENTRYPOINT ["sleep", "infinity"]
230
226
  `;
@@ -339,34 +335,26 @@ var copyTemplateFiles = (templateDir, destDir, mainFilename) => Effect.gen(funct
339
335
  { concurrency: "unbounded" }
340
336
  );
341
337
  });
342
- var rewriteMainTs = (configDir, agent, model, sandboxProvider, mainFilename, codexAuth) => Effect.gen(function* () {
338
+ var rewriteMainTs = (configDir, agent, model, modelExplicit, sandboxProvider, mainFilename, codexAuth) => Effect.gen(function* () {
343
339
  const fs = yield* FileSystem.FileSystem;
344
340
  const mainTsPath = join(configDir, mainFilename);
345
341
  const exists = yield* fs.exists(mainTsPath).pipe(Effect.mapError((e) => new Error(e.message)));
346
342
  if (!exists) return;
347
343
  let content = yield* fs.readFileString(mainTsPath).pipe(Effect.mapError((e) => new Error(e.message)));
348
- if (mainFilename === "main.ts") {
349
- content = content.replace(/main\.mts/g, "main.ts");
350
- }
351
344
  content = content.replace(
352
345
  new RegExp(`\\b${TEMPLATE_AGENT_FACTORY}\\b`, "g"),
353
346
  agent.factoryImport
354
347
  );
355
- const factoryCallRe = new RegExp(
356
- `${agent.factoryImport}\\(([^)\\n]*)\\)`,
357
- "g"
358
- );
359
348
  content = content.replace(
360
- factoryCallRe,
361
- (match, modelExpression) => {
362
- const keepsConfiguredModel = agent.name === "codex" && model === CODEX_MODELS.routine.model && /^(?:[A-Za-z_$][\w$]*\.)*CODEX_MODELS\.[A-Za-z_$][\w$]*$/.test(
363
- modelExpression.trim()
364
- );
365
- return keepsConfiguredModel ? match : `${agent.factoryImport}("${model}")`;
366
- }
349
+ "const CODEX_PROVIDER = true;",
350
+ `const CODEX_PROVIDER = ${agent.name === "codex"};`
367
351
  );
368
- if (agent.name !== "codex" || model !== CODEX_MODELS.routine.model) {
369
- content = content.replace(/\bCODEX_MODELS,\s*/g, "");
352
+ if (agent.name !== "codex" || modelExplicit) {
353
+ const modelLiteral = JSON.stringify(model);
354
+ content = content.replace(
355
+ /shipyard\.CODEX_MODELS\.(?:routine|strong)/g,
356
+ modelLiteral
357
+ );
370
358
  }
371
359
  if (agent.name === "codex" && codexAuth === "chatgpt") {
372
360
  content = content.replace(
@@ -434,13 +422,20 @@ var scaffold = (repoDir, options) => Effect.gen(function* () {
434
422
  const {
435
423
  agent,
436
424
  model,
437
- templateName = "blank",
425
+ modelExplicit = false,
426
+ templateName = "simple-loop",
438
427
  issueTracker = ISSUE_TRACKER_REGISTRY[0],
439
428
  // default: github-issues
440
429
  sandboxProvider = SANDBOX_PROVIDER_REGISTRY[0],
441
430
  // default: docker
442
431
  codexAuth = "api-key"
443
432
  } = options;
433
+ const totalProgressSteps = 5;
434
+ const reportProgress2 = (current, message) => options.onProgress?.({
435
+ current,
436
+ total: totalProgressSteps,
437
+ message
438
+ });
444
439
  if (codexAuth === "chatgpt" && agent.name !== "codex") {
445
440
  return yield* Effect.fail(
446
441
  new Error(
@@ -452,11 +447,14 @@ var scaffold = (repoDir, options) => Effect.gen(function* () {
452
447
  yield* assertConfigDirAvailable(repoDir);
453
448
  const configDir = join(repoDir, CONFIG_DIR);
454
449
  const mainFilename = yield* detectMainFilename(repoDir);
450
+ reportProgress2(1, "Validated repository and configuration");
455
451
  yield* fs.makeDirectory(configDir, { recursive: false }).pipe(Effect.mapError((e) => new Error(e.message)));
456
452
  const templateDir = yield* getTemplateDir(templateName);
453
+ reportProgress2(2, `Prepared ${CONFIG_DIR}/ directory`);
457
454
  const envExampleParts = [
458
455
  agent.name === "codex" && codexAuth === "chatgpt" ? CODEX_CHATGPT_ENV_EXAMPLE : agent.envExample
459
456
  ];
457
+ envExampleParts.push(roleModelEnvExample(agent.name));
460
458
  if (issueTracker.envExample) {
461
459
  envExampleParts.push(issueTracker.envExample);
462
460
  }
@@ -475,15 +473,19 @@ var scaffold = (repoDir, options) => Effect.gen(function* () {
475
473
  ],
476
474
  { concurrency: "unbounded" }
477
475
  );
476
+ reportProgress2(3, "Wrote configuration and template files");
478
477
  yield* rewriteMainTs(
479
478
  configDir,
480
479
  agent,
481
480
  model,
481
+ modelExplicit,
482
482
  sandboxProvider,
483
483
  mainFilename,
484
484
  codexAuth
485
485
  );
486
+ reportProgress2(4, "Configured selected agent and sandbox");
486
487
  yield* substituteTemplateArgs(configDir, issueTracker);
488
+ reportProgress2(5, "Applied issue tracker settings");
487
489
  return { mainFilename };
488
490
  });
489
491
  var resolveCodexAuthMode = async (options) => {
@@ -562,6 +564,11 @@ var ensureCodexChatGptAuth = (options) => {
562
564
  });
563
565
  }
564
566
  };
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
+ };
565
572
  var REPOSITORY_RUNNER_WORKFLOW_PATH = ".github/workflows/shipyard-wake.yml";
566
573
  var REPOSITORY_RUNNER_WAKE_EXECUTABLE = "shipyard-wake";
567
574
  var REPOSITORY_RUNNER_WORKFLOW = `name: Shipyard wake-up
@@ -756,6 +763,22 @@ var RELEASES_API = "https://api.github.com/repos/actions/runner/releases/latest"
756
763
  var RunnerInstallError = class extends Error {
757
764
  name = "RunnerInstallError";
758
765
  };
766
+ var RunnerInstallConflictError = class extends RunnerInstallError {
767
+ constructor(repository, runners) {
768
+ const names = runners.map(({ name }) => name).join(", ");
769
+ super(
770
+ `A repository runner labeled \`${ACTIVATION_LABEL}\` is already registered for ${repository}: ${names}. Remove it before installing another.`
771
+ );
772
+ this.repository = repository;
773
+ this.runners = runners;
774
+ const details = runners.map(
775
+ ({ id, name, status, busy }) => `${name}${id === void 0 ? "" : ` (#${id})`} (${status}${busy ? ", busy" : ""})`
776
+ ).join(", ");
777
+ this.confirmationMessage = `GitHub has ${runners.length} repository runner${runners.length === 1 ? "" : "s"} labeled \`${ACTIVATION_LABEL}\` for ${repository}: ${details}. Delete ${runners.length === 1 ? "this registration" : "these registrations"} and install a replacement?`;
778
+ }
779
+ name = "RunnerInstallConflictError";
780
+ confirmationMessage;
781
+ };
759
782
  var defaultAdapters = {
760
783
  platform: () => process.platform,
761
784
  arch: () => process.arch,
@@ -879,9 +902,7 @@ var selectArm64Asset = (release) => {
879
902
  var appendRunnerIgnores = async (gitignorePath, adapters) => {
880
903
  const current = await adapters.exists(gitignorePath) ? await adapters.readText(gitignorePath) : "";
881
904
  const lines = new Set(current.split(/\r?\n/));
882
- const additions = [`${RUNNER_DIR}/`, `${RUNNER_SANDBOX_MASK_DIR}/`].filter(
883
- (line) => !lines.has(line)
884
- );
905
+ const additions = [`${RUNNER_DIR}/`].filter((line) => !lines.has(line));
885
906
  if (additions.length === 0) return;
886
907
  const prefix = current.length === 0 || current.endsWith("\n") ? current : `${current}
887
908
  `;
@@ -892,6 +913,88 @@ var commandFailure = (purpose, error) => new RunnerInstallError(
892
913
  `${purpose} failed: ${error instanceof Error ? error.message : String(error)}`
893
914
  );
894
915
  var reportProgress = (options, current, total, message) => options.onProgress?.({ current, total, message });
916
+ var parseExistingRunnerRegistrations = (stdout) => {
917
+ const lines = stdout.trim().split(/\r?\n/).filter(Boolean);
918
+ return lines.map((line) => {
919
+ let value;
920
+ try {
921
+ value = JSON.parse(line);
922
+ } catch {
923
+ return { id: void 0, name: line, status: "unknown", busy: false };
924
+ }
925
+ if (typeof value !== "object" || value === null) {
926
+ throw new RunnerInstallError(
927
+ "GitHub returned invalid repository runner information."
928
+ );
929
+ }
930
+ const registration = value;
931
+ if (typeof registration.id !== "number" || !Number.isInteger(registration.id) || registration.id <= 0 || typeof registration.name !== "string" || registration.name.length === 0 || typeof registration.status !== "string" || typeof registration.busy !== "boolean") {
932
+ throw new RunnerInstallError(
933
+ "GitHub returned invalid repository runner information."
934
+ );
935
+ }
936
+ return {
937
+ id: registration.id,
938
+ name: registration.name,
939
+ status: registration.status,
940
+ busy: registration.busy
941
+ };
942
+ });
943
+ };
944
+ var removeExistingRepositoryRunnerRegistrations = async (options, adapters = defaultAdapters) => {
945
+ if (options.conflict.runners.some(({ id }) => id === void 0)) {
946
+ throw new RunnerInstallError(
947
+ "GitHub did not provide runner IDs, so Shipyard cannot safely remove the existing registrations. Remove them in GitHub Settings > Actions > Runners, then retry."
948
+ );
949
+ }
950
+ const environment = adapters.environment();
951
+ const removed = [];
952
+ for (const runner of options.conflict.runners) {
953
+ try {
954
+ await adapters.run(
955
+ "gh",
956
+ [
957
+ "api",
958
+ "--method",
959
+ "DELETE",
960
+ `repos/${options.conflict.repository}/actions/runners/${runner.id}`
961
+ ],
962
+ { cwd: options.repoDir, env: environment }
963
+ );
964
+ removed.push(runner.name);
965
+ } catch (error) {
966
+ const previous = removed.length === 0 ? "" : ` Earlier registrations were already removed: ${removed.join(", ")}.`;
967
+ throw new RunnerInstallError(
968
+ `${commandFailure(
969
+ `Removing GitHub runner ${runner.name} (#${runner.id}) from ${options.conflict.repository}`,
970
+ error
971
+ ).message}${previous}`
972
+ );
973
+ }
974
+ }
975
+ };
976
+ var installRepositoryRunnerWithReplacement = async (options, adapters = defaultAdapters) => {
977
+ try {
978
+ return await options.install();
979
+ } catch (error) {
980
+ if (!(error instanceof RunnerInstallConflictError) || !options.interactive) {
981
+ throw error;
982
+ }
983
+ if (!await options.confirmReplacement(error)) {
984
+ throw new RunnerInstallError(
985
+ `${error.message} Existing runner registrations were left unchanged; installation cancelled.`
986
+ );
987
+ }
988
+ await removeExistingRepositoryRunnerRegistrations(
989
+ {
990
+ repoDir: options.repoDir,
991
+ conflict: error
992
+ },
993
+ adapters
994
+ );
995
+ return options.install();
996
+ }
997
+ };
895
998
  var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
896
999
  if (adapters.platform() !== "darwin" || adapters.arch() !== "arm64") {
897
1000
  throw new RunnerInstallError(
@@ -900,7 +1003,6 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
900
1003
  }
901
1004
  const configDir = join(options.repoDir, CONFIG_DIR);
902
1005
  const runnerDir = join(configDir, RUNNER_DIR);
903
- const maskDir = join(configDir, RUNNER_SANDBOX_MASK_DIR);
904
1006
  if (!await adapters.exists(configDir)) {
905
1007
  throw new RunnerInstallError(
906
1008
  `No ${CONFIG_DIR}/ found. Run \`shipyard init\` in this repository first.`
@@ -956,7 +1058,6 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
956
1058
  {
957
1059
  repoDir: options.repoDir,
958
1060
  runnerDir,
959
- maskDir,
960
1061
  repository,
961
1062
  runnerName
962
1063
  },
@@ -969,10 +1070,8 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
969
1070
  }
970
1071
  await appendRunnerIgnores(join(configDir, ".gitignore"), adapters);
971
1072
  await adapters.chmod(runnerDir, 448);
972
- await adapters.chmod(maskDir, 448);
973
1073
  await assertProtectedDirectoryIdentities(
974
1074
  runnerDir,
975
- maskDir,
976
1075
  adapters.inspectDirectory
977
1076
  ).catch((error) => {
978
1077
  throw commandFailure("Validating protected runner directories", error);
@@ -1003,7 +1102,7 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1003
1102
  "--paginate",
1004
1103
  `repos/${repository}/actions/runners`,
1005
1104
  "--jq",
1006
- `.runners[] | select(any(.labels[]; .name == "${ACTIVATION_LABEL}")) | .name`
1105
+ `.runners[] | select(any(.labels[]; .name == "${ACTIVATION_LABEL}")) | {id, name, status, busy}`
1007
1106
  ],
1008
1107
  { cwd: options.repoDir, env: hostEnv }
1009
1108
  ).catch((error) => {
@@ -1012,10 +1111,11 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1012
1111
  error
1013
1112
  );
1014
1113
  });
1015
- if (existing.stdout.trim().length > 0) {
1016
- throw new RunnerInstallError(
1017
- `A repository runner labeled \`${ACTIVATION_LABEL}\` is already registered for ${repository}. Remove it before installing another.`
1018
- );
1114
+ const existingRegistrations = parseExistingRunnerRegistrations(
1115
+ existing.stdout
1116
+ );
1117
+ if (existingRegistrations.length > 0) {
1118
+ throw new RunnerInstallConflictError(repository, existingRegistrations);
1019
1119
  }
1020
1120
  reportProgress(
1021
1121
  options,
@@ -1088,18 +1188,13 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1088
1188
  await adapters.makeDirectory(runnerDir).catch((error) => {
1089
1189
  throw commandFailure("Creating the protected runner directory", error);
1090
1190
  });
1091
- if (!await adapters.exists(maskDir)) {
1092
- await adapters.makeDirectory(maskDir);
1093
- }
1094
1191
  await assertProtectedDirectoryIdentities(
1095
1192
  runnerDir,
1096
- maskDir,
1097
1193
  adapters.inspectDirectory
1098
1194
  ).catch((error) => {
1099
1195
  throw commandFailure("Validating protected runner directories", error);
1100
1196
  });
1101
1197
  await adapters.chmod(runnerDir, 448);
1102
- await adapters.chmod(maskDir, 448);
1103
1198
  reportProgress(
1104
1199
  options,
1105
1200
  5,
@@ -1139,15 +1234,12 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1139
1234
  { cwd: runnerDir, env: repositoryRunnerEnvironment(hostEnv) }
1140
1235
  );
1141
1236
  } catch {
1142
- const cleanup = await Promise.allSettled([
1143
- adapters.remove(runnerDir),
1144
- adapters.remove(maskDir)
1145
- ]);
1237
+ const cleanup = await Promise.allSettled([adapters.remove(runnerDir)]);
1146
1238
  const cleanupSucceeded = cleanup.every(
1147
1239
  (result) => result.status === "fulfilled"
1148
1240
  );
1149
1241
  throw new RunnerInstallError(
1150
- cleanupSucceeded ? `Registering ${runnerName} failed. Partial local runner files were removed so installation can be retried. Check GitHub Settings > Actions > Runners for an orphan registration; the one-time token was not stored.` : `Registering ${runnerName} failed and partial local runner files could not be fully removed. Remove only ${runnerDir} and ${maskDir}, check GitHub Settings > Actions > Runners for an orphan registration, then retry. The one-time token was not stored.`
1242
+ cleanupSucceeded ? `Registering ${runnerName} failed. Partial local runner files were removed so installation can be retried. Check GitHub Settings > Actions > Runners for an orphan registration; the one-time token was not stored.` : `Registering ${runnerName} failed and partial local runner files could not be fully removed. Remove only ${runnerDir}, check GitHub Settings > Actions > Runners for an orphan registration, then retry. The one-time token was not stored.`
1151
1243
  );
1152
1244
  }
1153
1245
  reportProgress(options, 7, progressTotal, "Registered runner with GitHub");
@@ -1389,7 +1481,6 @@ var requireRunnerContext = async (repoDir, adapters) => {
1389
1481
  }
1390
1482
  const configDir = join(repoDir, CONFIG_DIR);
1391
1483
  const runnerDir = join(configDir, RUNNER_DIR);
1392
- const maskDir = join(configDir, RUNNER_SANDBOX_MASK_DIR);
1393
1484
  if (!await adapters.exists(configDir)) {
1394
1485
  throw new RunnerControlError(
1395
1486
  `No ${CONFIG_DIR}/ found. Run \`shipyard init\` in this repository first.`
@@ -1539,7 +1630,6 @@ var requireRunnerContext = async (repoDir, adapters) => {
1539
1630
  });
1540
1631
  return {
1541
1632
  runnerDir,
1542
- maskDir,
1543
1633
  repository,
1544
1634
  metadata,
1545
1635
  hostEnvironment,
@@ -1593,7 +1683,6 @@ var startRepositoryRunnerManaged = async (options, adapters = defaultAdapters2)
1593
1683
  {
1594
1684
  repoDir: options.repoDir,
1595
1685
  runnerDir: context.runnerDir,
1596
- maskDir: context.maskDir,
1597
1686
  metadata: context.metadata,
1598
1687
  runnerEnvironment: repositoryRunnerEnvironment(context.hostEnvironment),
1599
1688
  dockerEnvironment: context.runtimeEnvironment
@@ -2123,19 +2212,38 @@ var installRunnerCommand = Command.make(
2123
2212
  { registrationToken: registrationTokenOption },
2124
2213
  ({ registrationToken }) => Effect.gen(function* () {
2125
2214
  const display = yield* Display;
2126
- const result = yield* display.progress(
2127
- "Installing repository runner",
2128
- (report) => Effect.tryPromise({
2129
- try: () => installRepositoryRunner({
2130
- repoDir: process.cwd(),
2131
- registrationToken: registrationToken._tag === "Some" ? registrationToken.value : void 0,
2132
- onProgress: report
2133
- }),
2134
- catch: (error) => new InitError({
2135
- message: error instanceof RunnerInstallError ? error.message : `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
2136
- })
2215
+ const repoDir = process.cwd();
2216
+ const result = yield* Effect.tryPromise({
2217
+ try: () => installRepositoryRunnerWithReplacement({
2218
+ repoDir,
2219
+ interactive: process.stdin.isTTY === true,
2220
+ confirmReplacement: async (conflict) => {
2221
+ const confirmed = await clack2.confirm({
2222
+ message: conflict.confirmationMessage,
2223
+ initialValue: false
2224
+ });
2225
+ return !clack2.isCancel(confirmed) && confirmed === true;
2226
+ },
2227
+ install: () => runEffectPromise(
2228
+ display.progress(
2229
+ "Installing repository runner",
2230
+ (report) => Effect.tryPromise({
2231
+ try: () => installRepositoryRunner({
2232
+ repoDir,
2233
+ registrationToken: registrationToken._tag === "Some" ? registrationToken.value : void 0,
2234
+ onProgress: report
2235
+ }),
2236
+ catch: (error) => error instanceof RunnerInstallError ? error : new InitError({
2237
+ message: `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
2238
+ })
2239
+ })
2240
+ )
2241
+ )
2242
+ }),
2243
+ catch: (error) => new InitError({
2244
+ message: error instanceof RunnerInstallError ? error.message : `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
2137
2245
  })
2138
- );
2246
+ });
2139
2247
  yield* display.status(
2140
2248
  `Installed ${result.name} for ${result.repository}.`,
2141
2249
  "success"
@@ -2259,7 +2367,7 @@ var runnerCommand = Command.make(
2259
2367
  purgeRunnerCommand
2260
2368
  ])
2261
2369
  );
2262
- var VERSION = "0.7.0" ;
2370
+ var VERSION = "0.8.0" ;
2263
2371
 
2264
2372
  // src/cli.ts
2265
2373
  var imageNameOption = Options.text("image-name").pipe(
@@ -2427,7 +2535,7 @@ var runCommand2 = Command.make(
2427
2535
  );
2428
2536
  var templateOption = Options.text("template").pipe(
2429
2537
  Options.withDescription(
2430
- "Template to scaffold (e.g. blank, simple-loop, parallel-planner)"
2538
+ "Template to scaffold (e.g. simple-loop, parallel-planner)"
2431
2539
  ),
2432
2540
  Options.optional
2433
2541
  );
@@ -2561,12 +2669,12 @@ var initCommand = Command.make(
2561
2669
  yield* failIfNonInteractive(params.flag);
2562
2670
  }
2563
2671
  const confirmed = yield* Effect.promise(
2564
- () => clack.confirm({
2672
+ () => clack2.confirm({
2565
2673
  message: params.promptMessage,
2566
2674
  initialValue: true
2567
2675
  })
2568
2676
  );
2569
- if (clack.isCancel(confirmed)) {
2677
+ if (clack2.isCancel(confirmed)) {
2570
2678
  yield* Effect.fail(
2571
2679
  new InitError({ message: params.cancelMessage })
2572
2680
  );
@@ -2591,7 +2699,7 @@ var initCommand = Command.make(
2591
2699
  yield* failIfNonInteractive("--agent");
2592
2700
  }
2593
2701
  const selected = yield* Effect.promise(
2594
- () => clack.select({
2702
+ () => clack2.select({
2595
2703
  message: "Select an agent:",
2596
2704
  initialValue: DEFAULT_AGENT_NAME,
2597
2705
  options: agents.map((a) => ({
@@ -2601,7 +2709,7 @@ var initCommand = Command.make(
2601
2709
  }))
2602
2710
  })
2603
2711
  );
2604
- if (clack.isCancel(selected)) {
2712
+ if (clack2.isCancel(selected)) {
2605
2713
  yield* Effect.fail(
2606
2714
  new InitError({ message: "Agent selection cancelled." })
2607
2715
  );
@@ -2614,7 +2722,7 @@ var initCommand = Command.make(
2614
2722
  requested: codexAuthFlag._tag === "Some" ? codexAuthFlag.value : void 0,
2615
2723
  interactive: isInteractive,
2616
2724
  select: async () => {
2617
- const selected = await clack.select({
2725
+ const selected = await clack2.select({
2618
2726
  message: "How will you authenticate Codex?",
2619
2727
  initialValue: "chatgpt",
2620
2728
  options: [
@@ -2630,7 +2738,7 @@ var initCommand = Command.make(
2630
2738
  }
2631
2739
  ]
2632
2740
  });
2633
- return clack.isCancel(selected) ? void 0 : selected;
2741
+ return clack2.isCancel(selected) ? void 0 : selected;
2634
2742
  }
2635
2743
  }),
2636
2744
  catch: (error) => error instanceof InitError ? error : new InitError({
@@ -2649,32 +2757,7 @@ var initCommand = Command.make(
2649
2757
  })
2650
2758
  });
2651
2759
  }
2652
- const sandboxProviders = listSandboxProviders();
2653
- let selectedSandboxProvider;
2654
- if (sandboxFlag._tag === "Some") {
2655
- selectedSandboxProvider = getSandboxProvider(sandboxFlag.value);
2656
- } else {
2657
- if (!isInteractive) {
2658
- yield* failIfNonInteractive("--sandbox");
2659
- }
2660
- const selected = yield* Effect.promise(
2661
- () => clack.select({
2662
- message: "Select a sandbox provider:",
2663
- options: sandboxProviders.map((p) => ({
2664
- value: p.name,
2665
- label: p.label
2666
- }))
2667
- })
2668
- );
2669
- if (clack.isCancel(selected)) {
2670
- yield* Effect.fail(
2671
- new InitError({
2672
- message: "Sandbox provider selection cancelled."
2673
- })
2674
- );
2675
- }
2676
- selectedSandboxProvider = getSandboxProvider(selected);
2677
- }
2760
+ const selectedSandboxProvider = getSandboxProvider("docker");
2678
2761
  const issueTrackers = listIssueTrackers();
2679
2762
  let selectedIssueTracker;
2680
2763
  if (issueTrackerFlag._tag === "Some") {
@@ -2684,7 +2767,7 @@ var initCommand = Command.make(
2684
2767
  yield* failIfNonInteractive("--issue-tracker");
2685
2768
  }
2686
2769
  const selected = yield* Effect.promise(
2687
- () => clack.select({
2770
+ () => clack2.select({
2688
2771
  message: "Select an issue tracker:",
2689
2772
  initialValue: "github-issues",
2690
2773
  options: issueTrackers.map((b) => ({
@@ -2693,7 +2776,7 @@ var initCommand = Command.make(
2693
2776
  }))
2694
2777
  })
2695
2778
  );
2696
- if (clack.isCancel(selected)) {
2779
+ if (clack2.isCancel(selected)) {
2697
2780
  yield* Effect.fail(
2698
2781
  new InitError({
2699
2782
  message: "Issue tracker selection cancelled."
@@ -2710,9 +2793,9 @@ var initCommand = Command.make(
2710
2793
  yield* failIfNonInteractive("--template");
2711
2794
  }
2712
2795
  const selected = yield* Effect.promise(
2713
- () => clack.select({
2796
+ () => clack2.select({
2714
2797
  message: "Select a template:",
2715
- initialValue: "blank",
2798
+ initialValue: "simple-loop",
2716
2799
  options: templates.map((tmpl) => ({
2717
2800
  value: tmpl.name,
2718
2801
  label: tmpl.name,
@@ -2720,7 +2803,7 @@ var initCommand = Command.make(
2720
2803
  }))
2721
2804
  })
2722
2805
  );
2723
- if (clack.isCancel(selected)) {
2806
+ if (clack2.isCancel(selected)) {
2724
2807
  yield* Effect.fail(
2725
2808
  new InitError({ message: "Template selection cancelled." })
2726
2809
  );
@@ -2772,15 +2855,17 @@ var initCommand = Command.make(
2772
2855
  console.warn(message);
2773
2856
  }
2774
2857
  }
2775
- yield* d.spinner(
2858
+ yield* d.progress(
2776
2859
  `Scaffolding ${CONFIG_DIR}/ config directory...`,
2777
- scaffold(cwd, {
2860
+ (report) => scaffold(cwd, {
2778
2861
  agent: selectedAgent,
2779
2862
  model: selectedModel,
2863
+ modelExplicit: modelFlag._tag === "Some",
2780
2864
  templateName: selectedTemplate,
2781
2865
  issueTracker: selectedIssueTracker,
2782
2866
  sandboxProvider: selectedSandboxProvider,
2783
- codexAuth: selectedCodexAuth
2867
+ codexAuth: selectedCodexAuth,
2868
+ onProgress: report
2784
2869
  }).pipe(
2785
2870
  Effect.mapError(
2786
2871
  (e) => new InitError({
@@ -2843,28 +2928,39 @@ var initCommand = Command.make(
2843
2928
  interactive: isInteractive,
2844
2929
  requested: installRunnerChoice._tag === "Some" ? installRunnerChoice.value : void 0,
2845
2930
  confirm: async ({ message, initialValue }) => {
2846
- const confirmed = await clack.confirm({ message, initialValue });
2847
- if (clack.isCancel(confirmed)) {
2931
+ const confirmed = await clack2.confirm({ message, initialValue });
2932
+ if (clack2.isCancel(confirmed)) {
2848
2933
  throw new InitError({
2849
2934
  message: "Repository-runner installation selection cancelled."
2850
2935
  });
2851
2936
  }
2852
2937
  return confirmed === true;
2853
2938
  },
2854
- install: () => Effect.runPromise(
2855
- d.progress(
2856
- "Installing repository runner",
2857
- (report) => Effect.tryPromise({
2858
- try: () => installRepositoryRunner({
2859
- repoDir: cwd,
2860
- onProgress: report
2861
- }),
2862
- catch: (error) => new InitError({
2863
- message: error instanceof RunnerInstallError ? error.message : `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
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
+ })
2864
2960
  })
2865
- })
2961
+ )
2866
2962
  )
2867
- )
2963
+ })
2868
2964
  }),
2869
2965
  catch: (error) => error instanceof InitError ? error : new InitError({
2870
2966
  message: `Repository-runner setup failed: ${error instanceof Error ? error.message : String(error)}`