@snappedly-tools/shipyard 0.5.0-staging.36022586813 → 0.5.0-staging.36028653877

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
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';
3
+ import { buildImage, removeImage, RunnerLifecycleError, removeRepositoryRunner, validateExistingRepositoryRunner, RUNNER_INSTALL_METADATA, 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, ConfigDirError, defaultImageName, ExecHostError, LOCKS_DIR, RUNNER_DIR, RUNNER_SANDBOX_MASK_DIR, assertProtectedDirectoryIdentities, repositoryRunnerEnvironment } from './chunk-FDYOTN55.js';
5
5
  import './chunk-KMGNFXKN.js';
6
6
  import './chunk-ACD46ZM4.js';
7
7
  import { NodeContext, NodeRuntime, NodeFileSystem } from '@effect/platform-node';
8
- import { Effect, Layer, Option } from 'effect';
8
+ import { Effect, Layer, Option, Exit, Cause } from 'effect';
9
9
  import { Options, Command } from '@effect/cli';
10
- import * as clack from '@clack/prompts';
10
+ import * as clack2 from '@clack/prompts';
11
11
  import { execFile, execSync, spawn, execFileSync } from 'child_process';
12
12
  import { existsSync, constants, statSync } from 'fs';
13
13
  import { join, resolve, dirname } from 'path';
@@ -441,6 +441,12 @@ var scaffold = (repoDir, options) => Effect.gen(function* () {
441
441
  // default: docker
442
442
  codexAuth = "api-key"
443
443
  } = options;
444
+ const totalProgressSteps = 5;
445
+ const reportProgress2 = (current, message) => options.onProgress?.({
446
+ current,
447
+ total: totalProgressSteps,
448
+ message
449
+ });
444
450
  if (codexAuth === "chatgpt" && agent.name !== "codex") {
445
451
  return yield* Effect.fail(
446
452
  new Error(
@@ -452,8 +458,10 @@ var scaffold = (repoDir, options) => Effect.gen(function* () {
452
458
  yield* assertConfigDirAvailable(repoDir);
453
459
  const configDir = join(repoDir, CONFIG_DIR);
454
460
  const mainFilename = yield* detectMainFilename(repoDir);
461
+ reportProgress2(1, "Validated repository and configuration");
455
462
  yield* fs.makeDirectory(configDir, { recursive: false }).pipe(Effect.mapError((e) => new Error(e.message)));
456
463
  const templateDir = yield* getTemplateDir(templateName);
464
+ reportProgress2(2, `Prepared ${CONFIG_DIR}/ directory`);
457
465
  const envExampleParts = [
458
466
  agent.name === "codex" && codexAuth === "chatgpt" ? CODEX_CHATGPT_ENV_EXAMPLE : agent.envExample
459
467
  ];
@@ -475,6 +483,7 @@ var scaffold = (repoDir, options) => Effect.gen(function* () {
475
483
  ],
476
484
  { concurrency: "unbounded" }
477
485
  );
486
+ reportProgress2(3, "Wrote configuration and template files");
478
487
  yield* rewriteMainTs(
479
488
  configDir,
480
489
  agent,
@@ -483,7 +492,9 @@ var scaffold = (repoDir, options) => Effect.gen(function* () {
483
492
  mainFilename,
484
493
  codexAuth
485
494
  );
495
+ reportProgress2(4, "Configured selected agent and sandbox");
486
496
  yield* substituteTemplateArgs(configDir, issueTracker);
497
+ reportProgress2(5, "Applied issue tracker settings");
487
498
  return { mainFilename };
488
499
  });
489
500
  var resolveCodexAuthMode = async (options) => {
@@ -562,6 +573,11 @@ var ensureCodexChatGptAuth = (options) => {
562
573
  });
563
574
  }
564
575
  };
576
+ var runEffectPromise = async (effect) => {
577
+ const exit = await Effect.runPromiseExit(effect);
578
+ if (Exit.isFailure(exit)) throw Cause.squash(exit.cause);
579
+ return exit.value;
580
+ };
565
581
  var REPOSITORY_RUNNER_WORKFLOW_PATH = ".github/workflows/shipyard-wake.yml";
566
582
  var REPOSITORY_RUNNER_WAKE_EXECUTABLE = "shipyard-wake";
567
583
  var REPOSITORY_RUNNER_WORKFLOW = `name: Shipyard wake-up
@@ -756,6 +772,22 @@ var RELEASES_API = "https://api.github.com/repos/actions/runner/releases/latest"
756
772
  var RunnerInstallError = class extends Error {
757
773
  name = "RunnerInstallError";
758
774
  };
775
+ var RunnerInstallConflictError = class extends RunnerInstallError {
776
+ constructor(repository, runners) {
777
+ const names = runners.map(({ name }) => name).join(", ");
778
+ super(
779
+ `A repository runner labeled \`${ACTIVATION_LABEL}\` is already registered for ${repository}: ${names}. Remove it before installing another.`
780
+ );
781
+ this.repository = repository;
782
+ this.runners = runners;
783
+ const details = runners.map(
784
+ ({ id, name, status, busy }) => `${name}${id === void 0 ? "" : ` (#${id})`} (${status}${busy ? ", busy" : ""})`
785
+ ).join(", ");
786
+ 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?`;
787
+ }
788
+ name = "RunnerInstallConflictError";
789
+ confirmationMessage;
790
+ };
759
791
  var defaultAdapters = {
760
792
  platform: () => process.platform,
761
793
  arch: () => process.arch,
@@ -892,6 +924,88 @@ var commandFailure = (purpose, error) => new RunnerInstallError(
892
924
  `${purpose} failed: ${error instanceof Error ? error.message : String(error)}`
893
925
  );
894
926
  var reportProgress = (options, current, total, message) => options.onProgress?.({ current, total, message });
927
+ var parseExistingRunnerRegistrations = (stdout) => {
928
+ const lines = stdout.trim().split(/\r?\n/).filter(Boolean);
929
+ return lines.map((line) => {
930
+ let value;
931
+ try {
932
+ value = JSON.parse(line);
933
+ } catch {
934
+ return { id: void 0, name: line, status: "unknown", busy: false };
935
+ }
936
+ if (typeof value !== "object" || value === null) {
937
+ throw new RunnerInstallError(
938
+ "GitHub returned invalid repository runner information."
939
+ );
940
+ }
941
+ const registration = value;
942
+ 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") {
943
+ throw new RunnerInstallError(
944
+ "GitHub returned invalid repository runner information."
945
+ );
946
+ }
947
+ return {
948
+ id: registration.id,
949
+ name: registration.name,
950
+ status: registration.status,
951
+ busy: registration.busy
952
+ };
953
+ });
954
+ };
955
+ var removeExistingRepositoryRunnerRegistrations = async (options, adapters = defaultAdapters) => {
956
+ if (options.conflict.runners.some(({ id }) => id === void 0)) {
957
+ throw new RunnerInstallError(
958
+ "GitHub did not provide runner IDs, so Shipyard cannot safely remove the existing registrations. Remove them in GitHub Settings > Actions > Runners, then retry."
959
+ );
960
+ }
961
+ const environment = adapters.environment();
962
+ const removed = [];
963
+ for (const runner of options.conflict.runners) {
964
+ try {
965
+ await adapters.run(
966
+ "gh",
967
+ [
968
+ "api",
969
+ "--method",
970
+ "DELETE",
971
+ `repos/${options.conflict.repository}/actions/runners/${runner.id}`
972
+ ],
973
+ { cwd: options.repoDir, env: environment }
974
+ );
975
+ removed.push(runner.name);
976
+ } catch (error) {
977
+ const previous = removed.length === 0 ? "" : ` Earlier registrations were already removed: ${removed.join(", ")}.`;
978
+ throw new RunnerInstallError(
979
+ `${commandFailure(
980
+ `Removing GitHub runner ${runner.name} (#${runner.id}) from ${options.conflict.repository}`,
981
+ error
982
+ ).message}${previous}`
983
+ );
984
+ }
985
+ }
986
+ };
987
+ var installRepositoryRunnerWithReplacement = async (options, adapters = defaultAdapters) => {
988
+ try {
989
+ return await options.install();
990
+ } catch (error) {
991
+ if (!(error instanceof RunnerInstallConflictError) || !options.interactive) {
992
+ throw error;
993
+ }
994
+ if (!await options.confirmReplacement(error)) {
995
+ throw new RunnerInstallError(
996
+ `${error.message} Existing runner registrations were left unchanged; installation cancelled.`
997
+ );
998
+ }
999
+ await removeExistingRepositoryRunnerRegistrations(
1000
+ {
1001
+ repoDir: options.repoDir,
1002
+ conflict: error
1003
+ },
1004
+ adapters
1005
+ );
1006
+ return options.install();
1007
+ }
1008
+ };
895
1009
  var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
896
1010
  if (adapters.platform() !== "darwin" || adapters.arch() !== "arm64") {
897
1011
  throw new RunnerInstallError(
@@ -1003,7 +1117,7 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1003
1117
  "--paginate",
1004
1118
  `repos/${repository}/actions/runners`,
1005
1119
  "--jq",
1006
- `.runners[] | select(any(.labels[]; .name == "${ACTIVATION_LABEL}")) | .name`
1120
+ `.runners[] | select(any(.labels[]; .name == "${ACTIVATION_LABEL}")) | {id, name, status, busy}`
1007
1121
  ],
1008
1122
  { cwd: options.repoDir, env: hostEnv }
1009
1123
  ).catch((error) => {
@@ -1012,10 +1126,11 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1012
1126
  error
1013
1127
  );
1014
1128
  });
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
- );
1129
+ const existingRegistrations = parseExistingRunnerRegistrations(
1130
+ existing.stdout
1131
+ );
1132
+ if (existingRegistrations.length > 0) {
1133
+ throw new RunnerInstallConflictError(repository, existingRegistrations);
1019
1134
  }
1020
1135
  reportProgress(
1021
1136
  options,
@@ -2123,19 +2238,38 @@ var installRunnerCommand = Command.make(
2123
2238
  { registrationToken: registrationTokenOption },
2124
2239
  ({ registrationToken }) => Effect.gen(function* () {
2125
2240
  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
- })
2241
+ const repoDir = process.cwd();
2242
+ const result = yield* Effect.tryPromise({
2243
+ try: () => installRepositoryRunnerWithReplacement({
2244
+ repoDir,
2245
+ interactive: process.stdin.isTTY === true,
2246
+ confirmReplacement: async (conflict) => {
2247
+ const confirmed = await clack2.confirm({
2248
+ message: conflict.confirmationMessage,
2249
+ initialValue: false
2250
+ });
2251
+ return !clack2.isCancel(confirmed) && confirmed === true;
2252
+ },
2253
+ install: () => runEffectPromise(
2254
+ display.progress(
2255
+ "Installing repository runner",
2256
+ (report) => Effect.tryPromise({
2257
+ try: () => installRepositoryRunner({
2258
+ repoDir,
2259
+ registrationToken: registrationToken._tag === "Some" ? registrationToken.value : void 0,
2260
+ onProgress: report
2261
+ }),
2262
+ catch: (error) => error instanceof RunnerInstallError ? error : new InitError({
2263
+ message: `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
2264
+ })
2265
+ })
2266
+ )
2267
+ )
2268
+ }),
2269
+ catch: (error) => new InitError({
2270
+ message: error instanceof RunnerInstallError ? error.message : `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
2137
2271
  })
2138
- );
2272
+ });
2139
2273
  yield* display.status(
2140
2274
  `Installed ${result.name} for ${result.repository}.`,
2141
2275
  "success"
@@ -2259,7 +2393,7 @@ var runnerCommand = Command.make(
2259
2393
  purgeRunnerCommand
2260
2394
  ])
2261
2395
  );
2262
- var VERSION = "0.5.0-staging.36022586813" ;
2396
+ var VERSION = "0.5.0-staging.36028653877" ;
2263
2397
 
2264
2398
  // src/cli.ts
2265
2399
  var imageNameOption = Options.text("image-name").pipe(
@@ -2561,12 +2695,12 @@ var initCommand = Command.make(
2561
2695
  yield* failIfNonInteractive(params.flag);
2562
2696
  }
2563
2697
  const confirmed = yield* Effect.promise(
2564
- () => clack.confirm({
2698
+ () => clack2.confirm({
2565
2699
  message: params.promptMessage,
2566
2700
  initialValue: true
2567
2701
  })
2568
2702
  );
2569
- if (clack.isCancel(confirmed)) {
2703
+ if (clack2.isCancel(confirmed)) {
2570
2704
  yield* Effect.fail(
2571
2705
  new InitError({ message: params.cancelMessage })
2572
2706
  );
@@ -2591,7 +2725,7 @@ var initCommand = Command.make(
2591
2725
  yield* failIfNonInteractive("--agent");
2592
2726
  }
2593
2727
  const selected = yield* Effect.promise(
2594
- () => clack.select({
2728
+ () => clack2.select({
2595
2729
  message: "Select an agent:",
2596
2730
  initialValue: DEFAULT_AGENT_NAME,
2597
2731
  options: agents.map((a) => ({
@@ -2601,7 +2735,7 @@ var initCommand = Command.make(
2601
2735
  }))
2602
2736
  })
2603
2737
  );
2604
- if (clack.isCancel(selected)) {
2738
+ if (clack2.isCancel(selected)) {
2605
2739
  yield* Effect.fail(
2606
2740
  new InitError({ message: "Agent selection cancelled." })
2607
2741
  );
@@ -2614,7 +2748,7 @@ var initCommand = Command.make(
2614
2748
  requested: codexAuthFlag._tag === "Some" ? codexAuthFlag.value : void 0,
2615
2749
  interactive: isInteractive,
2616
2750
  select: async () => {
2617
- const selected = await clack.select({
2751
+ const selected = await clack2.select({
2618
2752
  message: "How will you authenticate Codex?",
2619
2753
  initialValue: "chatgpt",
2620
2754
  options: [
@@ -2630,7 +2764,7 @@ var initCommand = Command.make(
2630
2764
  }
2631
2765
  ]
2632
2766
  });
2633
- return clack.isCancel(selected) ? void 0 : selected;
2767
+ return clack2.isCancel(selected) ? void 0 : selected;
2634
2768
  }
2635
2769
  }),
2636
2770
  catch: (error) => error instanceof InitError ? error : new InitError({
@@ -2658,7 +2792,7 @@ var initCommand = Command.make(
2658
2792
  yield* failIfNonInteractive("--sandbox");
2659
2793
  }
2660
2794
  const selected = yield* Effect.promise(
2661
- () => clack.select({
2795
+ () => clack2.select({
2662
2796
  message: "Select a sandbox provider:",
2663
2797
  options: sandboxProviders.map((p) => ({
2664
2798
  value: p.name,
@@ -2666,7 +2800,7 @@ var initCommand = Command.make(
2666
2800
  }))
2667
2801
  })
2668
2802
  );
2669
- if (clack.isCancel(selected)) {
2803
+ if (clack2.isCancel(selected)) {
2670
2804
  yield* Effect.fail(
2671
2805
  new InitError({
2672
2806
  message: "Sandbox provider selection cancelled."
@@ -2684,7 +2818,7 @@ var initCommand = Command.make(
2684
2818
  yield* failIfNonInteractive("--issue-tracker");
2685
2819
  }
2686
2820
  const selected = yield* Effect.promise(
2687
- () => clack.select({
2821
+ () => clack2.select({
2688
2822
  message: "Select an issue tracker:",
2689
2823
  initialValue: "github-issues",
2690
2824
  options: issueTrackers.map((b) => ({
@@ -2693,7 +2827,7 @@ var initCommand = Command.make(
2693
2827
  }))
2694
2828
  })
2695
2829
  );
2696
- if (clack.isCancel(selected)) {
2830
+ if (clack2.isCancel(selected)) {
2697
2831
  yield* Effect.fail(
2698
2832
  new InitError({
2699
2833
  message: "Issue tracker selection cancelled."
@@ -2710,7 +2844,7 @@ var initCommand = Command.make(
2710
2844
  yield* failIfNonInteractive("--template");
2711
2845
  }
2712
2846
  const selected = yield* Effect.promise(
2713
- () => clack.select({
2847
+ () => clack2.select({
2714
2848
  message: "Select a template:",
2715
2849
  initialValue: "blank",
2716
2850
  options: templates.map((tmpl) => ({
@@ -2720,7 +2854,7 @@ var initCommand = Command.make(
2720
2854
  }))
2721
2855
  })
2722
2856
  );
2723
- if (clack.isCancel(selected)) {
2857
+ if (clack2.isCancel(selected)) {
2724
2858
  yield* Effect.fail(
2725
2859
  new InitError({ message: "Template selection cancelled." })
2726
2860
  );
@@ -2772,15 +2906,16 @@ var initCommand = Command.make(
2772
2906
  console.warn(message);
2773
2907
  }
2774
2908
  }
2775
- yield* d.spinner(
2909
+ yield* d.progress(
2776
2910
  `Scaffolding ${CONFIG_DIR}/ config directory...`,
2777
- scaffold(cwd, {
2911
+ (report) => scaffold(cwd, {
2778
2912
  agent: selectedAgent,
2779
2913
  model: selectedModel,
2780
2914
  templateName: selectedTemplate,
2781
2915
  issueTracker: selectedIssueTracker,
2782
2916
  sandboxProvider: selectedSandboxProvider,
2783
- codexAuth: selectedCodexAuth
2917
+ codexAuth: selectedCodexAuth,
2918
+ onProgress: report
2784
2919
  }).pipe(
2785
2920
  Effect.mapError(
2786
2921
  (e) => new InitError({
@@ -2843,28 +2978,39 @@ var initCommand = Command.make(
2843
2978
  interactive: isInteractive,
2844
2979
  requested: installRunnerChoice._tag === "Some" ? installRunnerChoice.value : void 0,
2845
2980
  confirm: async ({ message, initialValue }) => {
2846
- const confirmed = await clack.confirm({ message, initialValue });
2847
- if (clack.isCancel(confirmed)) {
2981
+ const confirmed = await clack2.confirm({ message, initialValue });
2982
+ if (clack2.isCancel(confirmed)) {
2848
2983
  throw new InitError({
2849
2984
  message: "Repository-runner installation selection cancelled."
2850
2985
  });
2851
2986
  }
2852
2987
  return confirmed === true;
2853
2988
  },
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)}`
2989
+ install: () => installRepositoryRunnerWithReplacement({
2990
+ repoDir: cwd,
2991
+ interactive: isInteractive,
2992
+ confirmReplacement: async (conflict) => {
2993
+ const confirmed = await clack2.confirm({
2994
+ message: conflict.confirmationMessage,
2995
+ initialValue: false
2996
+ });
2997
+ return !clack2.isCancel(confirmed) && confirmed === true;
2998
+ },
2999
+ install: () => runEffectPromise(
3000
+ d.progress(
3001
+ "Installing repository runner",
3002
+ (report) => Effect.tryPromise({
3003
+ try: () => installRepositoryRunner({
3004
+ repoDir: cwd,
3005
+ onProgress: report
3006
+ }),
3007
+ catch: (error) => error instanceof RunnerInstallError ? error : new InitError({
3008
+ message: `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
3009
+ })
2864
3010
  })
2865
- })
3011
+ )
2866
3012
  )
2867
- )
3013
+ })
2868
3014
  }),
2869
3015
  catch: (error) => error instanceof InitError ? error : new InitError({
2870
3016
  message: `Repository-runner setup failed: ${error instanceof Error ? error.message : String(error)}`