@snappedly-tools/shipyard 0.7.0 → 0.9.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 (63) hide show
  1. package/README.md +46 -43
  2. package/dist/{MountConfig-bZoCs4Dd.d.ts → MountConfig-K5ILnfht.d.ts} +1 -1
  3. package/dist/SandboxProvider-oUAwYlWm.d.ts +120 -0
  4. package/dist/{chunk-FDYOTN55.js → chunk-57EEKW3R.js} +361 -984
  5. package/dist/chunk-57EEKW3R.js.map +1 -0
  6. package/dist/{chunk-56TDSWFU.js → chunk-HXSZM52J.js} +3 -3
  7. package/dist/{chunk-56TDSWFU.js.map → chunk-HXSZM52J.js.map} +1 -1
  8. package/dist/{chunk-WAXEMZUV.js → chunk-JZUBT4WG.js} +241 -33
  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 +99 -2606
  16. package/dist/index.js +412 -7487
  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 +826 -472
  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 -3
  28. package/dist/sandboxes/docker.js +2 -4
  29. package/dist/templates/parallel-planner/main.mts +78 -16
  30. package/dist/templates/parallel-planner/planner-branch.mts +151 -0
  31. package/dist/templates/parallel-planner/setup.sh +1 -0
  32. package/dist/templates/parallel-planner-with-review/main.mts +81 -19
  33. package/dist/templates/parallel-planner-with-review/planner-branch.mts +151 -0
  34. package/dist/templates/parallel-planner-with-review/setup.sh +1 -0
  35. package/dist/templates/sequential-reviewer/main.mts +59 -6
  36. package/dist/templates/sequential-reviewer/setup.sh +1 -0
  37. package/dist/templates/shared/setup.sh +1 -0
  38. package/dist/templates/simple-loop/main.mts +58 -5
  39. package/dist/templates/simple-loop/setup.sh +1 -0
  40. package/dist/workflow/coordinator/migrations/002_workflow_phase_records.sql +11 -0
  41. package/dist/workflow/coordinator/migrations/003_phase_record_schema_version.sql +6 -0
  42. package/dist/workflow.d.ts +472 -0
  43. package/dist/workflow.js +4345 -0
  44. package/dist/workflow.js.map +1 -0
  45. package/package.json +11 -15
  46. package/dist/SandboxProvider-XJQqEdSf.d.ts +0 -261
  47. package/dist/chunk-ACD46ZM4.js +0 -136
  48. package/dist/chunk-ACD46ZM4.js.map +0 -1
  49. package/dist/chunk-FDYOTN55.js.map +0 -1
  50. package/dist/chunk-KMGNFXKN.js +0 -38
  51. package/dist/chunk-KMGNFXKN.js.map +0 -1
  52. package/dist/chunk-SOJTAJTF.js +0 -78
  53. package/dist/chunk-SOJTAJTF.js.map +0 -1
  54. package/dist/chunk-WAXEMZUV.js.map +0 -1
  55. package/dist/sandboxes/no-sandbox.d.ts +0 -37
  56. package/dist/sandboxes/no-sandbox.js +0 -4
  57. package/dist/sandboxes/no-sandbox.js.map +0 -1
  58. package/dist/sandboxes/vercel.d.ts +0 -104
  59. package/dist/sandboxes/vercel.js +0 -166
  60. package/dist/sandboxes/vercel.js.map +0 -1
  61. package/dist/templates/blank/main.mts +0 -13
  62. package/dist/templates/blank/prompt.md +0 -12
  63. 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-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';
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';
@@ -16,21 +14,15 @@ import { FileSystem } from '@effect/platform';
16
14
  import { fileURLToPath } from 'url';
17
15
  import { hostname, homedir } from 'os';
18
16
  import { createHash } from 'crypto';
19
- 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';
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"
@@ -105,6 +101,12 @@ var LOCKFILES = [
105
101
  ["yarn.lock", "yarn"],
106
102
  ["package-lock.json", "npm"]
107
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
+ };
108
110
  var detectPackageManager = (repoDir) => Effect.gen(function* () {
109
111
  const fs = yield* FileSystem.FileSystem;
110
112
  const pkgPath = join(repoDir, "package.json");
@@ -128,18 +130,8 @@ var detectPackageManager = (repoDir) => Effect.gen(function* () {
128
130
  }
129
131
  return "npm";
130
132
  });
131
- var addDependencyCommand = (packageManager, pkg) => {
132
- switch (packageManager) {
133
- case "pnpm":
134
- return `pnpm add ${pkg}`;
135
- case "yarn":
136
- return `yarn add ${pkg}`;
137
- case "bun":
138
- return `bun add ${pkg}`;
139
- case "npm":
140
- return `npm install ${pkg}`;
141
- }
142
- };
133
+ var addDependencyCommand = (packageManager, pkg) => `${packageManager} ${DEPENDENCY_COMMANDS[packageManager].add} ${pkg}`;
134
+ var removeDependencyCommand = (packageManager, pkg) => `${packageManager} ${DEPENDENCY_COMMANDS[packageManager].remove} ${pkg}`;
143
135
  var hostHasDependency = (repoDir, pkg) => Effect.gen(function* () {
144
136
  const fs = yield* FileSystem.FileSystem;
145
137
  const pkgPath = join(repoDir, "package.json");
@@ -175,7 +167,7 @@ RUN apt-get update && apt-get install -y \\
175
167
 
176
168
  # Build-args for UID/GID alignment: shipyard docker build-image
177
169
  # defaults these to the host user's UID/GID so image-built files
178
- # and bind-mounted files share an owner without runtime chown.
170
+ # and explicit mounted files share an owner without runtime chown.
179
171
  ARG AGENT_UID=1000
180
172
  ARG AGENT_GID=1000
181
173
 
@@ -191,8 +183,8 @@ ENV PATH="/home/agent/.local/bin:$PATH"
191
183
 
192
184
  WORKDIR /home/agent
193
185
 
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.
186
+ # Shipyard syncs Git history into a sandbox-owned repository at ${SANDBOX_REPO_DIR}
187
+ # and runs agent commands from ${SANDBOX_REPO_DIR}.
196
188
  # Structure your Dockerfile so that ${SANDBOX_REPO_DIR} can serve as the project root.
197
189
  ENTRYPOINT ["sleep", "infinity"]
198
190
  `;
@@ -209,7 +201,7 @@ RUN apt-get update && apt-get install -y \\
209
201
 
210
202
  # Build-args for UID/GID alignment: shipyard docker build-image
211
203
  # defaults these to the host user's UID/GID so image-built files
212
- # and bind-mounted files share an owner without runtime chown.
204
+ # and explicit mounted files share an owner without runtime chown.
213
205
  ARG AGENT_UID=1000
214
206
  ARG AGENT_GID=1000
215
207
 
@@ -223,8 +215,8 @@ USER \${AGENT_UID}:\${AGENT_GID}
223
215
 
224
216
  WORKDIR /home/agent
225
217
 
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.
218
+ # Shipyard syncs Git history into a sandbox-owned repository at ${SANDBOX_REPO_DIR}
219
+ # and runs agent commands from ${SANDBOX_REPO_DIR}.
228
220
  # Structure your Dockerfile so that ${SANDBOX_REPO_DIR} can serve as the project root.
229
221
  ENTRYPOINT ["sleep", "infinity"]
230
222
  `;
@@ -275,11 +267,9 @@ var ISSUE_TRACKER_REGISTRY = [
275
267
  # Required repository permissions: Contents, Issues, and Pull requests (Read and write); Metadata (Read)
276
268
  # Or leave blank and run: GH_TOKEN="$(gh auth token)" npx shipyard run
277
269
  GH_TOKEN=
278
- # GitHub repository (owner/repository)
279
- GH_REPO=`
270
+ `
280
271
  }
281
272
  ];
282
- var listIssueTrackers = () => ISSUE_TRACKER_REGISTRY;
283
273
  var getIssueTracker = (name) => ISSUE_TRACKER_REGISTRY.find((b) => b.name === name);
284
274
  var getAgent = (name) => AGENT_REGISTRY.find((a) => a.name === name);
285
275
  var SANDBOX_PROVIDER_REGISTRY = [
@@ -299,7 +289,7 @@ function getNextStepsLines() {
299
289
  "2. If using a model subscription, sign in. For Codex:",
300
290
  ` codex --config 'cli_auth_credentials_store="file"' login`,
301
291
  " test -f ~/.codex/auth.json",
302
- `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\`.`
303
293
  ];
304
294
  }
305
295
  function getTemplatesDir() {
@@ -339,39 +329,31 @@ var copyTemplateFiles = (templateDir, destDir, mainFilename) => Effect.gen(funct
339
329
  { concurrency: "unbounded" }
340
330
  );
341
331
  });
342
- var rewriteMainTs = (configDir, agent, model, sandboxProvider, mainFilename, codexAuth) => Effect.gen(function* () {
332
+ var rewriteMainTs = (configDir, agent, model, modelExplicit, sandboxProvider, mainFilename, codexAuth) => Effect.gen(function* () {
343
333
  const fs = yield* FileSystem.FileSystem;
344
334
  const mainTsPath = join(configDir, mainFilename);
345
335
  const exists = yield* fs.exists(mainTsPath).pipe(Effect.mapError((e) => new Error(e.message)));
346
336
  if (!exists) return;
347
337
  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
338
  content = content.replace(
352
339
  new RegExp(`\\b${TEMPLATE_AGENT_FACTORY}\\b`, "g"),
353
340
  agent.factoryImport
354
341
  );
355
- const factoryCallRe = new RegExp(
356
- `${agent.factoryImport}\\(([^)\\n]*)\\)`,
357
- "g"
358
- );
359
342
  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
- }
343
+ "const CODEX_PROVIDER = true;",
344
+ `const CODEX_PROVIDER = ${agent.name === "codex"};`
367
345
  );
368
- if (agent.name !== "codex" || model !== CODEX_MODELS.routine.model) {
369
- content = content.replace(/\bCODEX_MODELS,\s*/g, "");
346
+ if (agent.name !== "codex" || modelExplicit) {
347
+ const modelLiteral = JSON.stringify(model);
348
+ content = content.replace(
349
+ /shipyard\.CODEX_MODELS\.(?:routine|strong)/g,
350
+ modelLiteral
351
+ );
370
352
  }
371
353
  if (agent.name === "codex" && codexAuth === "chatgpt") {
372
354
  content = content.replace(
373
- /\bdocker\(\)/g,
374
- `docker(${CODEX_CHATGPT_AUTH_OPTIONS})`
355
+ "const sandboxAuthOptions = {};",
356
+ `const sandboxAuthOptions = ${CODEX_CHATGPT_AUTH_OPTIONS};`
375
357
  );
376
358
  }
377
359
  content = content.replace(/\bdocker\b/g, sandboxProvider.name);
@@ -434,13 +416,20 @@ var scaffold = (repoDir, options) => Effect.gen(function* () {
434
416
  const {
435
417
  agent,
436
418
  model,
437
- templateName = "blank",
419
+ modelExplicit = false,
420
+ templateName = "simple-loop",
438
421
  issueTracker = ISSUE_TRACKER_REGISTRY[0],
439
422
  // default: github-issues
440
423
  sandboxProvider = SANDBOX_PROVIDER_REGISTRY[0],
441
424
  // default: docker
442
425
  codexAuth = "api-key"
443
426
  } = options;
427
+ const totalProgressSteps = 5;
428
+ const reportProgress2 = (current, message) => options.onProgress?.({
429
+ current,
430
+ total: totalProgressSteps,
431
+ message
432
+ });
444
433
  if (codexAuth === "chatgpt" && agent.name !== "codex") {
445
434
  return yield* Effect.fail(
446
435
  new Error(
@@ -452,11 +441,14 @@ var scaffold = (repoDir, options) => Effect.gen(function* () {
452
441
  yield* assertConfigDirAvailable(repoDir);
453
442
  const configDir = join(repoDir, CONFIG_DIR);
454
443
  const mainFilename = yield* detectMainFilename(repoDir);
444
+ reportProgress2(1, "Validated repository and configuration");
455
445
  yield* fs.makeDirectory(configDir, { recursive: false }).pipe(Effect.mapError((e) => new Error(e.message)));
456
446
  const templateDir = yield* getTemplateDir(templateName);
447
+ reportProgress2(2, `Prepared ${CONFIG_DIR}/ directory`);
457
448
  const envExampleParts = [
458
449
  agent.name === "codex" && codexAuth === "chatgpt" ? CODEX_CHATGPT_ENV_EXAMPLE : agent.envExample
459
450
  ];
451
+ envExampleParts.push(roleModelEnvExample(agent.name));
460
452
  if (issueTracker.envExample) {
461
453
  envExampleParts.push(issueTracker.envExample);
462
454
  }
@@ -475,15 +467,19 @@ var scaffold = (repoDir, options) => Effect.gen(function* () {
475
467
  ],
476
468
  { concurrency: "unbounded" }
477
469
  );
470
+ reportProgress2(3, "Wrote configuration and template files");
478
471
  yield* rewriteMainTs(
479
472
  configDir,
480
473
  agent,
481
474
  model,
475
+ modelExplicit,
482
476
  sandboxProvider,
483
477
  mainFilename,
484
478
  codexAuth
485
479
  );
480
+ reportProgress2(4, "Configured selected agent and sandbox");
486
481
  yield* substituteTemplateArgs(configDir, issueTracker);
482
+ reportProgress2(5, "Applied issue tracker settings");
487
483
  return { mainFilename };
488
484
  });
489
485
  var resolveCodexAuthMode = async (options) => {
@@ -756,6 +752,22 @@ var RELEASES_API = "https://api.github.com/repos/actions/runner/releases/latest"
756
752
  var RunnerInstallError = class extends Error {
757
753
  name = "RunnerInstallError";
758
754
  };
755
+ var RunnerInstallConflictError = class extends RunnerInstallError {
756
+ constructor(repository, runners) {
757
+ const names = runners.map(({ name }) => name).join(", ");
758
+ super(
759
+ `A repository runner labeled \`${ACTIVATION_LABEL}\` is already registered for ${repository}: ${names}. Remove it before installing another.`
760
+ );
761
+ this.repository = repository;
762
+ this.runners = runners;
763
+ const details = runners.map(
764
+ ({ id, name, status, busy }) => `${name}${id === void 0 ? "" : ` (#${id})`} (${status}${busy ? ", busy" : ""})`
765
+ ).join(", ");
766
+ 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?`;
767
+ }
768
+ name = "RunnerInstallConflictError";
769
+ confirmationMessage;
770
+ };
759
771
  var defaultAdapters = {
760
772
  platform: () => process.platform,
761
773
  arch: () => process.arch,
@@ -879,9 +891,7 @@ var selectArm64Asset = (release) => {
879
891
  var appendRunnerIgnores = async (gitignorePath, adapters) => {
880
892
  const current = await adapters.exists(gitignorePath) ? await adapters.readText(gitignorePath) : "";
881
893
  const lines = new Set(current.split(/\r?\n/));
882
- const additions = [`${RUNNER_DIR}/`, `${RUNNER_SANDBOX_MASK_DIR}/`].filter(
883
- (line) => !lines.has(line)
884
- );
894
+ const additions = [`${RUNNER_DIR}/`].filter((line) => !lines.has(line));
885
895
  if (additions.length === 0) return;
886
896
  const prefix = current.length === 0 || current.endsWith("\n") ? current : `${current}
887
897
  `;
@@ -892,6 +902,88 @@ var commandFailure = (purpose, error) => new RunnerInstallError(
892
902
  `${purpose} failed: ${error instanceof Error ? error.message : String(error)}`
893
903
  );
894
904
  var reportProgress = (options, current, total, message) => options.onProgress?.({ current, total, message });
905
+ var parseExistingRunnerRegistrations = (stdout) => {
906
+ const lines = stdout.trim().split(/\r?\n/).filter(Boolean);
907
+ return lines.map((line) => {
908
+ let value;
909
+ try {
910
+ value = JSON.parse(line);
911
+ } catch {
912
+ return { id: void 0, name: line, status: "unknown", busy: false };
913
+ }
914
+ if (typeof value !== "object" || value === null) {
915
+ throw new RunnerInstallError(
916
+ "GitHub returned invalid repository runner information."
917
+ );
918
+ }
919
+ const registration = value;
920
+ 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") {
921
+ throw new RunnerInstallError(
922
+ "GitHub returned invalid repository runner information."
923
+ );
924
+ }
925
+ return {
926
+ id: registration.id,
927
+ name: registration.name,
928
+ status: registration.status,
929
+ busy: registration.busy
930
+ };
931
+ });
932
+ };
933
+ var removeExistingRepositoryRunnerRegistrations = async (options, adapters = defaultAdapters) => {
934
+ if (options.conflict.runners.some(({ id }) => id === void 0)) {
935
+ throw new RunnerInstallError(
936
+ "GitHub did not provide runner IDs, so Shipyard cannot safely remove the existing registrations. Remove them in GitHub Settings > Actions > Runners, then retry."
937
+ );
938
+ }
939
+ const environment = adapters.environment();
940
+ const removed = [];
941
+ for (const runner of options.conflict.runners) {
942
+ try {
943
+ await adapters.run(
944
+ "gh",
945
+ [
946
+ "api",
947
+ "--method",
948
+ "DELETE",
949
+ `repos/${options.conflict.repository}/actions/runners/${runner.id}`
950
+ ],
951
+ { cwd: options.repoDir, env: environment }
952
+ );
953
+ removed.push(runner.name);
954
+ } catch (error) {
955
+ const previous = removed.length === 0 ? "" : ` Earlier registrations were already removed: ${removed.join(", ")}.`;
956
+ throw new RunnerInstallError(
957
+ `${commandFailure(
958
+ `Removing GitHub runner ${runner.name} (#${runner.id}) from ${options.conflict.repository}`,
959
+ error
960
+ ).message}${previous}`
961
+ );
962
+ }
963
+ }
964
+ };
965
+ var installRepositoryRunnerWithReplacement = async (options, adapters = defaultAdapters) => {
966
+ try {
967
+ return await options.install();
968
+ } catch (error) {
969
+ if (!(error instanceof RunnerInstallConflictError) || !options.interactive) {
970
+ throw error;
971
+ }
972
+ if (!await options.confirmReplacement(error)) {
973
+ throw new RunnerInstallError(
974
+ `${error.message} Existing runner registrations were left unchanged; installation cancelled.`
975
+ );
976
+ }
977
+ await removeExistingRepositoryRunnerRegistrations(
978
+ {
979
+ repoDir: options.repoDir,
980
+ conflict: error
981
+ },
982
+ adapters
983
+ );
984
+ return options.install();
985
+ }
986
+ };
895
987
  var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
896
988
  if (adapters.platform() !== "darwin" || adapters.arch() !== "arm64") {
897
989
  throw new RunnerInstallError(
@@ -900,7 +992,6 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
900
992
  }
901
993
  const configDir = join(options.repoDir, CONFIG_DIR);
902
994
  const runnerDir = join(configDir, RUNNER_DIR);
903
- const maskDir = join(configDir, RUNNER_SANDBOX_MASK_DIR);
904
995
  if (!await adapters.exists(configDir)) {
905
996
  throw new RunnerInstallError(
906
997
  `No ${CONFIG_DIR}/ found. Run \`shipyard init\` in this repository first.`
@@ -956,7 +1047,6 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
956
1047
  {
957
1048
  repoDir: options.repoDir,
958
1049
  runnerDir,
959
- maskDir,
960
1050
  repository,
961
1051
  runnerName
962
1052
  },
@@ -969,10 +1059,8 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
969
1059
  }
970
1060
  await appendRunnerIgnores(join(configDir, ".gitignore"), adapters);
971
1061
  await adapters.chmod(runnerDir, 448);
972
- await adapters.chmod(maskDir, 448);
973
1062
  await assertProtectedDirectoryIdentities(
974
1063
  runnerDir,
975
- maskDir,
976
1064
  adapters.inspectDirectory
977
1065
  ).catch((error) => {
978
1066
  throw commandFailure("Validating protected runner directories", error);
@@ -1003,7 +1091,7 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1003
1091
  "--paginate",
1004
1092
  `repos/${repository}/actions/runners`,
1005
1093
  "--jq",
1006
- `.runners[] | select(any(.labels[]; .name == "${ACTIVATION_LABEL}")) | .name`
1094
+ `.runners[] | select(any(.labels[]; .name == "${ACTIVATION_LABEL}")) | {id, name, status, busy}`
1007
1095
  ],
1008
1096
  { cwd: options.repoDir, env: hostEnv }
1009
1097
  ).catch((error) => {
@@ -1012,10 +1100,11 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1012
1100
  error
1013
1101
  );
1014
1102
  });
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
- );
1103
+ const existingRegistrations = parseExistingRunnerRegistrations(
1104
+ existing.stdout
1105
+ );
1106
+ if (existingRegistrations.length > 0) {
1107
+ throw new RunnerInstallConflictError(repository, existingRegistrations);
1019
1108
  }
1020
1109
  reportProgress(
1021
1110
  options,
@@ -1088,18 +1177,13 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1088
1177
  await adapters.makeDirectory(runnerDir).catch((error) => {
1089
1178
  throw commandFailure("Creating the protected runner directory", error);
1090
1179
  });
1091
- if (!await adapters.exists(maskDir)) {
1092
- await adapters.makeDirectory(maskDir);
1093
- }
1094
1180
  await assertProtectedDirectoryIdentities(
1095
1181
  runnerDir,
1096
- maskDir,
1097
1182
  adapters.inspectDirectory
1098
1183
  ).catch((error) => {
1099
1184
  throw commandFailure("Validating protected runner directories", error);
1100
1185
  });
1101
1186
  await adapters.chmod(runnerDir, 448);
1102
- await adapters.chmod(maskDir, 448);
1103
1187
  reportProgress(
1104
1188
  options,
1105
1189
  5,
@@ -1139,15 +1223,12 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1139
1223
  { cwd: runnerDir, env: repositoryRunnerEnvironment(hostEnv) }
1140
1224
  );
1141
1225
  } catch {
1142
- const cleanup = await Promise.allSettled([
1143
- adapters.remove(runnerDir),
1144
- adapters.remove(maskDir)
1145
- ]);
1226
+ const cleanup = await Promise.allSettled([adapters.remove(runnerDir)]);
1146
1227
  const cleanupSucceeded = cleanup.every(
1147
1228
  (result) => result.status === "fulfilled"
1148
1229
  );
1149
1230
  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.`
1231
+ 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
1232
  );
1152
1233
  }
1153
1234
  reportProgress(options, 7, progressTotal, "Registered runner with GitHub");
@@ -1182,31 +1263,58 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1182
1263
  };
1183
1264
 
1184
1265
  // src/InitRepositoryRunner.ts
1185
- var initializeRepositoryRunner = async (options) => {
1186
- let shouldInstall = options.requested;
1187
- if (shouldInstall === void 0) {
1188
- shouldInstall = options.interactive ? await options.confirm({
1189
- message: "Install a foreground repository runner for labelled GitHub issues?",
1190
- initialValue: true
1191
- }) : false;
1192
- }
1193
- if (!shouldInstall) return { status: "declined" };
1194
- try {
1195
- return { status: "installed", result: await options.install() };
1196
- } catch (error) {
1197
- return {
1198
- status: "failed",
1199
- message: error instanceof Error ? error.message : String(error)
1200
- };
1201
- }
1202
- };
1203
1266
  var repositoryRunnerNextSteps = () => [
1204
- `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`,
1205
1268
  `Keep the foreground controller open with \`npx ${CLI_NAME} runner start\``,
1206
1269
  `Inspect it from another terminal with \`npx ${CLI_NAME} runner status\``,
1207
1270
  `Stop it with \`npx ${CLI_NAME} runner stop\``,
1208
1271
  `Unregister and delete its protected files with \`npx ${CLI_NAME} runner remove\``
1209
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
+ };
1210
1318
  var execFileAsync2 = promisify(execFile);
1211
1319
  var CONTROLLER_STATE = ".shipyard-state.json";
1212
1320
  var LAST_FAILURE = ".shipyard-last-failure.json";
@@ -1389,7 +1497,6 @@ var requireRunnerContext = async (repoDir, adapters) => {
1389
1497
  }
1390
1498
  const configDir = join(repoDir, CONFIG_DIR);
1391
1499
  const runnerDir = join(configDir, RUNNER_DIR);
1392
- const maskDir = join(configDir, RUNNER_SANDBOX_MASK_DIR);
1393
1500
  if (!await adapters.exists(configDir)) {
1394
1501
  throw new RunnerControlError(
1395
1502
  `No ${CONFIG_DIR}/ found. Run \`shipyard init\` in this repository first.`
@@ -1539,7 +1646,6 @@ var requireRunnerContext = async (repoDir, adapters) => {
1539
1646
  });
1540
1647
  return {
1541
1648
  runnerDir,
1542
- maskDir,
1543
1649
  repository,
1544
1650
  metadata,
1545
1651
  hostEnvironment,
@@ -1567,7 +1673,6 @@ var stopChildBounded = async (child, pause) => {
1567
1673
  );
1568
1674
  }
1569
1675
  };
1570
- 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;
1571
1676
  var bestEffort = async (operation) => {
1572
1677
  try {
1573
1678
  await operation();
@@ -1593,7 +1698,6 @@ var startRepositoryRunnerManaged = async (options, adapters = defaultAdapters2)
1593
1698
  {
1594
1699
  repoDir: options.repoDir,
1595
1700
  runnerDir: context.runnerDir,
1596
- maskDir: context.maskDir,
1597
1701
  metadata: context.metadata,
1598
1702
  runnerEnvironment: repositoryRunnerEnvironment(context.hostEnvironment),
1599
1703
  dockerEnvironment: context.runtimeEnvironment
@@ -2110,6 +2214,11 @@ var stopRepositoryRunner = async (options, adapters = defaultAdapters2) => {
2110
2214
  }
2111
2215
  return { pid: lock.pid };
2112
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
+ };
2113
2222
 
2114
2223
  // src/RepositoryRunnerCommands.ts
2115
2224
  var registrationTokenOption = Options.text("registration-token").pipe(
@@ -2123,19 +2232,38 @@ var installRunnerCommand = Command.make(
2123
2232
  { registrationToken: registrationTokenOption },
2124
2233
  ({ registrationToken }) => Effect.gen(function* () {
2125
2234
  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
- })
2235
+ const repoDir = process.cwd();
2236
+ const result = yield* Effect.tryPromise({
2237
+ try: () => installRepositoryRunnerWithReplacement({
2238
+ repoDir,
2239
+ interactive: process.stdin.isTTY === true,
2240
+ confirmReplacement: async (conflict) => {
2241
+ const confirmed = await clack2.confirm({
2242
+ message: conflict.confirmationMessage,
2243
+ initialValue: false
2244
+ });
2245
+ return !clack2.isCancel(confirmed) && confirmed === true;
2246
+ },
2247
+ install: () => runEffectPromise(
2248
+ display.progress(
2249
+ "Installing repository runner",
2250
+ (report) => Effect.tryPromise({
2251
+ try: () => installRepositoryRunner({
2252
+ repoDir,
2253
+ registrationToken: registrationToken._tag === "Some" ? registrationToken.value : void 0,
2254
+ onProgress: report
2255
+ }),
2256
+ catch: (error) => error instanceof RunnerInstallError ? error : new InitError({
2257
+ message: `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
2258
+ })
2259
+ })
2260
+ )
2261
+ )
2262
+ }),
2263
+ catch: (error) => new InitError({
2264
+ message: error instanceof RunnerInstallError ? error.message : `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
2137
2265
  })
2138
- );
2266
+ });
2139
2267
  yield* display.status(
2140
2268
  `Installed ${result.name} for ${result.repository}.`,
2141
2269
  "success"
@@ -2259,7 +2387,78 @@ var runnerCommand = Command.make(
2259
2387
  purgeRunnerCommand
2260
2388
  ])
2261
2389
  );
2262
- var VERSION = "0.7.0" ;
2390
+ var VERSION = "0.9.0" ;
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
+ };
2263
2462
 
2264
2463
  // src/cli.ts
2265
2464
  var imageNameOption = Options.text("image-name").pipe(
@@ -2275,7 +2474,6 @@ var defaultUidBuildArgs = () => {
2275
2474
  if (gid !== void 0) args.AGENT_GID = String(gid);
2276
2475
  return args;
2277
2476
  };
2278
- var requireConfigDir = (cwd) => requireCanonicalConfigDir(cwd);
2279
2477
  var purgeRunLogsBestEffort = (display, repoDir) => Effect.tryPromise({
2280
2478
  try: () => purgeRunLogs({
2281
2479
  repoDir,
@@ -2395,7 +2593,7 @@ var runCommand2 = Command.make(
2395
2593
  ({ imageName: imageNameFlag, sandbox, skipBuild, entrypoint }) => Effect.gen(function* () {
2396
2594
  const d = yield* Display;
2397
2595
  const cwd = process.cwd();
2398
- const configDir = yield* requireConfigDir(cwd);
2596
+ const configDir = yield* requireCanonicalConfigDir(cwd);
2399
2597
  yield* purgeRunLogsBestEffort(d, cwd);
2400
2598
  const resolvedEntrypoint = resolveRunEntrypoint(
2401
2599
  cwd,
@@ -2427,7 +2625,7 @@ var runCommand2 = Command.make(
2427
2625
  );
2428
2626
  var templateOption = Options.text("template").pipe(
2429
2627
  Options.withDescription(
2430
- "Template to scaffold (e.g. blank, simple-loop, parallel-planner)"
2628
+ "Template to scaffold (e.g. simple-loop, parallel-planner)"
2431
2629
  ),
2432
2630
  Options.optional
2433
2631
  );
@@ -2454,14 +2652,6 @@ var sandboxOption = Options.text("sandbox").pipe(
2454
2652
  Options.withDescription("Sandbox provider to use (docker)"),
2455
2653
  Options.optional
2456
2654
  );
2457
- var issueTrackerOption = Options.text("issue-tracker").pipe(
2458
- Options.withDescription("Issue tracker to use (github-issues)"),
2459
- Options.optional
2460
- );
2461
- var buildImageOption = Options.choice("build-image", ["true", "false"]).pipe(
2462
- Options.withDescription("Whether to build the sandbox image now"),
2463
- Options.optional
2464
- );
2465
2655
  var installTemplateDepsOption = Options.choice("install-template-deps", [
2466
2656
  "true",
2467
2657
  "false"
@@ -2471,14 +2661,18 @@ var installTemplateDepsOption = Options.choice("install-template-deps", [
2471
2661
  ),
2472
2662
  Options.optional
2473
2663
  );
2474
- var installRunnerOption = Options.choice("install-runner", [
2475
- "true",
2476
- "false"
2477
- ]).pipe(
2664
+ var commitSetupOption = Options.choice("commit-setup", ["true", "false"]).pipe(
2478
2665
  Options.withDescription(
2479
- "Whether to install a foreground repository runner after scaffolding"
2480
- ),
2481
- 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
+ )
2482
2676
  );
2483
2677
  var choiceToTriBool = (opt) => opt._tag === "Some" ? Option.some(opt.value === "true") : Option.none();
2484
2678
  var initCommand = Command.make(
@@ -2490,10 +2684,8 @@ var initCommand = Command.make(
2490
2684
  codexAuth: codexAuthOption,
2491
2685
  model: initModelOption,
2492
2686
  sandbox: sandboxOption,
2493
- issueTracker: issueTrackerOption,
2494
- buildImage: buildImageOption,
2495
2687
  installTemplateDeps: installTemplateDepsOption,
2496
- installRunner: installRunnerOption
2688
+ commitSetup: commitSetupOption
2497
2689
  },
2498
2690
  ({
2499
2691
  imageName: imageNameFlag,
@@ -2502,10 +2694,8 @@ var initCommand = Command.make(
2502
2694
  codexAuth: codexAuthFlag,
2503
2695
  model: modelFlag,
2504
2696
  sandbox: sandboxFlag,
2505
- issueTracker: issueTrackerFlag,
2506
- buildImage: buildImageFlag,
2507
2697
  installTemplateDeps: installTemplateDepsFlag,
2508
- installRunner: installRunnerFlag
2698
+ commitSetup: commitSetupFlag
2509
2699
  }) => Effect.gen(function* () {
2510
2700
  const d = yield* Display;
2511
2701
  const cwd = process.cwd();
@@ -2533,367 +2723,530 @@ var initCommand = Command.make(
2533
2723
  );
2534
2724
  }
2535
2725
  }
2536
- if (issueTrackerFlag._tag === "Some") {
2537
- const valid = getIssueTracker(issueTrackerFlag.value);
2538
- if (!valid) {
2539
- const names = listIssueTrackers().map((t) => t.name).join(", ");
2540
- yield* Effect.fail(
2541
- new InitError({
2542
- message: `Unknown issue tracker "${issueTrackerFlag.value}". Available: ${names}`
2543
- })
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())
2544
2750
  );
2545
- }
2546
- }
2547
- const buildImageChoice = choiceToTriBool(buildImageFlag);
2548
- const installTemplateDepsChoice = choiceToTriBool(
2549
- installTemplateDepsFlag
2550
- );
2551
- const installRunnerChoice = choiceToTriBool(installRunnerFlag);
2552
- const isInteractive = process.stdin.isTTY === true;
2553
- const failIfNonInteractive = (flag) => Effect.fail(
2554
- new InitError({
2555
- message: `${flag} is required in non-interactive mode (no TTY detected).`
2556
- })
2557
- );
2558
- const resolveConfirmFlag = (params) => Effect.gen(function* () {
2559
- if (params.choice._tag === "Some") return params.choice.value;
2560
- if (!isInteractive) {
2561
- yield* failIfNonInteractive(params.flag);
2562
- }
2563
- const confirmed = yield* Effect.promise(
2564
- () => clack.confirm({
2565
- message: params.promptMessage,
2566
- initialValue: true
2567
- })
2568
- );
2569
- if (clack.isCancel(confirmed)) {
2570
- yield* Effect.fail(
2571
- new InitError({ message: params.cancelMessage })
2751
+ const installTemplateDepsChoice = choiceToTriBool(
2752
+ installTemplateDepsFlag
2572
2753
  );
2573
- }
2574
- return confirmed === true;
2575
- });
2576
- const agents = listAgents();
2577
- let selectedAgent;
2578
- if (agentFlag._tag === "Some") {
2579
- const entry = getAgent(agentFlag.value);
2580
- if (!entry) {
2581
- const names = agents.map((a) => a.name).join(", ");
2582
- yield* Effect.fail(
2754
+ const commitSetupChoice = choiceToTriBool(commitSetupFlag);
2755
+ const isInteractive = process.stdin.isTTY === true;
2756
+ const failIfNonInteractive = (flag) => Effect.fail(
2583
2757
  new InitError({
2584
- message: `Unknown agent "${agentFlag.value}". Available: ${names}`
2758
+ message: `${flag} is required in non-interactive mode (no TTY detected).`
2585
2759
  })
2586
2760
  );
2587
- }
2588
- selectedAgent = entry;
2589
- } else {
2590
- if (!isInteractive) {
2591
- yield* failIfNonInteractive("--agent");
2592
- }
2593
- const selected = yield* Effect.promise(
2594
- () => clack.select({
2595
- message: "Select an agent:",
2596
- initialValue: DEFAULT_AGENT_NAME,
2597
- options: agents.map((a) => ({
2598
- value: a.name,
2599
- label: a.label,
2600
- hint: `Default model: ${a.defaultModel}`
2601
- }))
2602
- })
2603
- );
2604
- if (clack.isCancel(selected)) {
2605
- yield* Effect.fail(
2606
- new InitError({ message: "Agent selection cancelled." })
2607
- );
2608
- }
2609
- selectedAgent = getAgent(selected);
2610
- }
2611
- const selectedCodexAuth = yield* Effect.tryPromise({
2612
- try: () => resolveCodexAuthMode({
2613
- agentName: selectedAgent.name,
2614
- requested: codexAuthFlag._tag === "Some" ? codexAuthFlag.value : void 0,
2615
- interactive: isInteractive,
2616
- select: async () => {
2617
- const selected = await clack.select({
2618
- message: "How will you authenticate Codex?",
2619
- initialValue: "chatgpt",
2620
- options: [
2621
- {
2622
- value: "chatgpt",
2623
- label: "Sign in with ChatGPT",
2624
- hint: "Use your ChatGPT subscription"
2625
- },
2626
- {
2627
- value: "api-key",
2628
- label: "OpenAI API key",
2629
- 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();
2630
2863
  }
2631
- ]
2864
+ },
2865
+ catch: (error) => error instanceof InitError ? error : new InitError({
2866
+ message: `Codex authentication setup failed: ${String(error)}`
2867
+ })
2632
2868
  });
2633
- return clack.isCancel(selected) ? void 0 : selected;
2634
2869
  }
2635
- }),
2636
- catch: (error) => error instanceof InitError ? error : new InitError({
2637
- message: `Codex authentication selection failed: ${String(error)}`
2638
- })
2639
- });
2640
- const selectedModel = modelFlag._tag === "Some" ? modelFlag.value : selectedAgent.defaultModel;
2641
- if (selectedCodexAuth === "chatgpt") {
2642
- yield* Effect.try({
2643
- try: () => ensureCodexChatGptAuth({
2644
- cwd,
2645
- interactive: isInteractive
2646
- }),
2647
- catch: (error) => error instanceof InitError ? error : new InitError({
2648
- message: `Codex authentication setup failed: ${String(error)}`
2649
- })
2650
- });
2651
- }
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
- }
2678
- const issueTrackers = listIssueTrackers();
2679
- let selectedIssueTracker;
2680
- if (issueTrackerFlag._tag === "Some") {
2681
- selectedIssueTracker = getIssueTracker(issueTrackerFlag.value);
2682
- } else {
2683
- if (!isInteractive) {
2684
- yield* failIfNonInteractive("--issue-tracker");
2685
- }
2686
- const selected = yield* Effect.promise(
2687
- () => clack.select({
2688
- message: "Select an issue tracker:",
2689
- initialValue: "github-issues",
2690
- options: issueTrackers.map((b) => ({
2691
- value: b.name,
2692
- label: b.label
2693
- }))
2694
- })
2695
- );
2696
- if (clack.isCancel(selected)) {
2697
- yield* Effect.fail(
2698
- new InitError({
2699
- message: "Issue tracker selection cancelled."
2700
- })
2701
- );
2702
- }
2703
- selectedIssueTracker = getIssueTracker(selected);
2704
- }
2705
- let selectedTemplate;
2706
- if (template._tag === "Some") {
2707
- selectedTemplate = template.value;
2708
- } else {
2709
- if (!isInteractive) {
2710
- yield* failIfNonInteractive("--template");
2711
- }
2712
- const selected = yield* Effect.promise(
2713
- () => clack.select({
2714
- message: "Select a template:",
2715
- initialValue: "blank",
2716
- options: templates.map((tmpl) => ({
2717
- value: tmpl.name,
2718
- label: tmpl.name,
2719
- hint: tmpl.description
2720
- }))
2721
- })
2722
- );
2723
- if (clack.isCancel(selected)) {
2724
- yield* Effect.fail(
2725
- new InitError({ message: "Template selection cancelled." })
2726
- );
2727
- }
2728
- selectedTemplate = selected;
2729
- }
2730
- if (selectedIssueTracker.name === "github-issues") {
2731
- const failedLabels = [];
2732
- for (const [name, description, color] of [
2733
- [ACTIVATION_LABEL, `Issues for ${PRODUCT_NAME} to work on`, "F9A825"],
2734
- ["bug", "Something is broken", "D73A4A"],
2735
- ["enhancement", "New feature or improvement", "A2EEEF"],
2736
- ["needs-triage", "Maintainer evaluation needed", "FBCA04"],
2737
- ["needs-info", "Waiting for reporter information", "D4C5F9"],
2738
- ["ready-for-agent", "Ready for agent implementation", "0E8A16"],
2739
- ["ready-for-human", "Requires human implementation", "1D76DB"],
2740
- ["wontfix", "Will not be actioned", "FFFFFF"],
2741
- ["shipyard:blocked", "Shipyard work needs intervention", "B60205"],
2742
- ["shipyard:pending", "Shipyard is working on this ticket", "1D76DB"],
2743
- [
2744
- "shipyard:complete",
2745
- "Shipyard work ready for human review",
2746
- "0E8A16"
2747
- ],
2748
- [
2749
- "shipyard:outstanding-tasks",
2750
- "Spec has uncompleted tickets",
2751
- "FBCA04"
2752
- ]
2753
- ]) {
2754
- try {
2755
- execSync(
2756
- `gh label create "${name}" --description "${description}" --color "${color}" --force`,
2757
- { 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
+ )
2758
2892
  );
2759
- } catch {
2760
- failedLabels.push(name);
2893
+ if (clack2.isCancel(selected)) {
2894
+ yield* Effect.fail(
2895
+ new InitError({ message: "Template selection cancelled." })
2896
+ );
2897
+ }
2898
+ selectedTemplate = selected;
2761
2899
  }
2762
- }
2763
- if (failedLabels.length) {
2764
- let connected = Boolean(process.env.GH_REPO);
2765
- try {
2766
- execSync("git remote get-url origin", { cwd, stdio: "ignore" });
2767
- connected = true;
2768
- } 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
+ );
2769
2945
  }
2770
- const message = `Could not create GitHub labels: ${failedLabels.join(", ")}. Check GitHub access and rerun init.`;
2771
- if (connected) yield* Effect.fail(new InitError({ message }));
2772
- console.warn(message);
2773
- }
2774
- }
2775
- yield* d.spinner(
2776
- `Scaffolding ${CONFIG_DIR}/ config directory...`,
2777
- scaffold(cwd, {
2778
- agent: selectedAgent,
2779
- model: selectedModel,
2780
- templateName: selectedTemplate,
2781
- issueTracker: selectedIssueTracker,
2782
- sandboxProvider: selectedSandboxProvider,
2783
- codexAuth: selectedCodexAuth
2784
- }).pipe(
2785
- Effect.mapError(
2786
- (e) => new InitError({
2787
- 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\`.`
2788
3047
  })
2789
- )
2790
- )
2791
- );
2792
- const packageManager = yield* detectPackageManager(cwd);
2793
- if (getTemplateDependencies(selectedTemplate).includes("zod")) {
2794
- const alreadyInstalled = yield* hostHasDependency(cwd, "zod");
2795
- if (!alreadyInstalled) {
2796
- const installCmd = addDependencyCommand(packageManager, "zod");
2797
- const shouldInstall = yield* resolveConfirmFlag({
2798
- choice: installTemplateDepsChoice,
2799
- flag: "--install-template-deps",
2800
- promptMessage: `The ${selectedTemplate} template needs a schema validator. Install zod now (\`${installCmd}\`)?`,
2801
- cancelMessage: "Install-template-deps selection cancelled."
2802
3048
  });
2803
- if (shouldInstall) {
2804
- 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(() => {
2805
3079
  try {
2806
- execSync(installCmd, { cwd, stdio: "ignore" });
2807
- return true;
2808
- } catch {
2809
- 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
+ };
2810
3089
  }
2811
3090
  });
2812
- yield* installed ? d.status(`Installed zod with ${packageManager}.`, "success") : d.status(
2813
- `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.",
2814
3123
  "warn"
2815
3124
  );
2816
3125
  }
2817
- }
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}`));
2818
3132
  }
2819
- const providerLabel = selectedSandboxProvider.label;
2820
- const shouldBuild = yield* resolveConfirmFlag({
2821
- choice: buildImageChoice,
2822
- flag: "--build-image",
2823
- promptMessage: `Build the default ${providerLabel} image now?`,
2824
- cancelMessage: "Build-image selection cancelled."
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));
3137
+ }
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
+ })
2825
3151
  });
2826
- if (shouldBuild) {
2827
- const containerfileDir = join(cwd, CONFIG_DIR);
2828
- yield* d.spinner(
2829
- `Building ${providerLabel} image '${imageName}'...`,
2830
- buildImage(imageName, containerfileDir, {
2831
- buildArgs: defaultUidBuildArgs()
2832
- })
2833
- );
2834
- yield* d.status("Image built successfully.", "success");
2835
- } 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) {
2836
3162
  yield* d.status(
2837
- `Run \`${CLI_NAME} ${selectedSandboxProvider.cliNamespace} build-image\` to build the ${providerLabel} image later.`,
3163
+ "No Shipyard installation found in this repository.",
2838
3164
  "info"
2839
3165
  );
3166
+ return;
2840
3167
  }
2841
- const runnerInit = yield* Effect.tryPromise({
2842
- try: () => initializeRepositoryRunner({
2843
- interactive: isInteractive,
2844
- requested: installRunnerChoice._tag === "Some" ? installRunnerChoice.value : void 0,
2845
- confirm: async ({ message, initialValue }) => {
2846
- const confirmed = await clack.confirm({ message, initialValue });
2847
- if (clack.isCancel(confirmed)) {
2848
- throw new InitError({
2849
- message: "Repository-runner installation selection cancelled."
2850
- });
2851
- }
2852
- return confirmed === true;
2853
- },
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)}`
2864
- })
2865
- })
2866
- )
2867
- )
2868
- }),
2869
- catch: (error) => error instanceof InitError ? error : new InitError({
2870
- message: `Repository-runner setup failed: ${error instanceof Error ? error.message : String(error)}`
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."
3172
+ })
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)}`
2871
3212
  })
2872
3213
  });
2873
- if (runnerInit.status === "installed") {
3214
+ if (files.configDirectoryRemoved) {
3215
+ yield* d.status(`Removed all of ${CONFIG_DIR}/.`, "success");
3216
+ }
3217
+ if (files.workflowRemoved) {
2874
3218
  yield* d.status(
2875
- `Installed ${runnerInit.result.name} for ${runnerInit.result.repository}.`,
3219
+ `Removed ${REPOSITORY_RUNNER_WORKFLOW_PATH}.`,
2876
3220
  "success"
2877
3221
  );
2878
- yield* d.text("Repository runner next steps:");
2879
- for (const [index, line] of repositoryRunnerNextSteps().entries()) {
2880
- yield* d.text(styleText("dim", `${index + 1}. ${line}`));
2881
- }
2882
- } else if (runnerInit.status === "failed") {
2883
3222
  yield* d.status(
2884
- `Shipyard scaffolding is ready, but repository runner installation failed: ${runnerInit.message}`,
2885
- "warn"
3223
+ "Commit and push the workflow deletion to disable it on GitHub.",
3224
+ "info"
2886
3225
  );
3226
+ } else if (files.workflowPreserved) {
2887
3227
  yield* d.status(
2888
- `Retry from this repository with \`npx ${CLI_NAME} runner install\`.`,
3228
+ `Preserved ${REPOSITORY_RUNNER_WORKFLOW_PATH} because it is customized or linked.`,
2889
3229
  "warn"
2890
3230
  );
2891
3231
  }
2892
- yield* d.status("Init complete!", "success");
2893
- const nextSteps = getNextStepsLines();
2894
- for (const [i, line] of nextSteps.entries()) {
2895
- 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
+ );
2896
3248
  }
3249
+ yield* d.status("Shipyard uninstalled from this repository.", "success");
2897
3250
  })
2898
3251
  );
2899
3252
  var dockerfileOption = Options.file("dockerfile").pipe(
@@ -2911,7 +3264,7 @@ var buildImageCommand = Command.make(
2911
3264
  ({ imageName: imageNameFlag, dockerfile }) => Effect.gen(function* () {
2912
3265
  const d = yield* Display;
2913
3266
  const cwd = process.cwd();
2914
- yield* requireConfigDir(cwd);
3267
+ yield* requireCanonicalConfigDir(cwd);
2915
3268
  const imageName = resolveImageName(imageNameFlag, cwd);
2916
3269
  const dockerfileDir = join(cwd, CONFIG_DIR);
2917
3270
  const dockerfilePath = dockerfile._tag === "Some" ? dockerfile.value : void 0;
@@ -2964,6 +3317,7 @@ var rootCommand = Command.make(
2964
3317
  var shipyard = rootCommand.pipe(
2965
3318
  Command.withSubcommands([
2966
3319
  initCommand,
3320
+ uninstallCommand,
2967
3321
  runCommand2,
2968
3322
  dockerCommand,
2969
3323
  runnerCommand