@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.
- package/README.md +46 -43
- package/dist/{MountConfig-bZoCs4Dd.d.ts → MountConfig-K5ILnfht.d.ts} +1 -1
- package/dist/SandboxProvider-oUAwYlWm.d.ts +120 -0
- package/dist/{chunk-FDYOTN55.js → chunk-57EEKW3R.js} +361 -984
- package/dist/chunk-57EEKW3R.js.map +1 -0
- package/dist/{chunk-56TDSWFU.js → chunk-HXSZM52J.js} +3 -3
- package/dist/{chunk-56TDSWFU.js.map → chunk-HXSZM52J.js.map} +1 -1
- package/dist/{chunk-WAXEMZUV.js → chunk-JZUBT4WG.js} +241 -33
- package/dist/chunk-JZUBT4WG.js.map +1 -0
- package/dist/chunk-NQRFVKCU.js +1840 -0
- package/dist/chunk-NQRFVKCU.js.map +1 -0
- package/dist/chunk-Z5C4LHVP.js +137 -0
- package/dist/chunk-Z5C4LHVP.js.map +1 -0
- package/dist/createSandbox-DmbnWAZv.d.ts +739 -0
- package/dist/index.d.ts +99 -2606
- package/dist/index.js +412 -7487
- package/dist/index.js.map +1 -1
- package/dist/integrations/github.d.ts +52 -0
- package/dist/integrations/github.js +1049 -0
- package/dist/integrations/github.js.map +1 -0
- package/dist/integrations/releases.d.ts +136 -0
- package/dist/integrations/releases.js +500 -0
- package/dist/integrations/releases.js.map +1 -0
- package/dist/main.js +826 -472
- package/dist/main.js.map +1 -1
- package/dist/publication-BPoy_M9M.d.ts +1200 -0
- package/dist/sandboxes/docker.d.ts +2 -3
- package/dist/sandboxes/docker.js +2 -4
- package/dist/templates/parallel-planner/main.mts +78 -16
- package/dist/templates/parallel-planner/planner-branch.mts +151 -0
- package/dist/templates/parallel-planner/setup.sh +1 -0
- package/dist/templates/parallel-planner-with-review/main.mts +81 -19
- package/dist/templates/parallel-planner-with-review/planner-branch.mts +151 -0
- package/dist/templates/parallel-planner-with-review/setup.sh +1 -0
- package/dist/templates/sequential-reviewer/main.mts +59 -6
- package/dist/templates/sequential-reviewer/setup.sh +1 -0
- package/dist/templates/shared/setup.sh +1 -0
- package/dist/templates/simple-loop/main.mts +58 -5
- package/dist/templates/simple-loop/setup.sh +1 -0
- package/dist/workflow/coordinator/migrations/002_workflow_phase_records.sql +11 -0
- package/dist/workflow/coordinator/migrations/003_phase_record_schema_version.sql +6 -0
- package/dist/workflow.d.ts +472 -0
- package/dist/workflow.js +4345 -0
- package/dist/workflow.js.map +1 -0
- package/package.json +11 -15
- package/dist/SandboxProvider-XJQqEdSf.d.ts +0 -261
- package/dist/chunk-ACD46ZM4.js +0 -136
- package/dist/chunk-ACD46ZM4.js.map +0 -1
- package/dist/chunk-FDYOTN55.js.map +0 -1
- package/dist/chunk-KMGNFXKN.js +0 -38
- package/dist/chunk-KMGNFXKN.js.map +0 -1
- package/dist/chunk-SOJTAJTF.js +0 -78
- package/dist/chunk-SOJTAJTF.js.map +0 -1
- package/dist/chunk-WAXEMZUV.js.map +0 -1
- package/dist/sandboxes/no-sandbox.d.ts +0 -37
- package/dist/sandboxes/no-sandbox.js +0 -4
- package/dist/sandboxes/no-sandbox.js.map +0 -1
- package/dist/sandboxes/vercel.d.ts +0 -104
- package/dist/sandboxes/vercel.js +0 -166
- package/dist/sandboxes/vercel.js.map +0 -1
- package/dist/templates/blank/main.mts +0 -13
- package/dist/templates/blank/prompt.md +0 -12
- 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-
|
|
3
|
-
import { buildImage, removeImage,
|
|
4
|
-
import { SANDBOX_REPO_DIR, Display, InitError, ACTIVATION_LABEL, PRODUCT_NAME, CONFIG_DIR, CLI_NAME,
|
|
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
|
|
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,
|
|
17
|
+
import { chmod, rm, lstat, realpath, mkdir, writeFile, readFile, access } from 'fs/promises';
|
|
20
18
|
import 'module';
|
|
21
19
|
|
|
22
|
-
var
|
|
20
|
+
var configDirExists = (repoDir) => Effect.gen(function* () {
|
|
23
21
|
const fs = yield* FileSystem.FileSystem;
|
|
24
|
-
return yield* fs.exists(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
195
|
-
# and
|
|
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
|
|
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
|
-
#
|
|
227
|
-
# and
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
361
|
-
|
|
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" ||
|
|
369
|
-
|
|
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
|
-
|
|
374
|
-
`
|
|
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
|
-
|
|
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}
|
|
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}")) |
|
|
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
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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}
|
|
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
|
-
`
|
|
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
|
|
2127
|
-
|
|
2128
|
-
(
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
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.
|
|
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*
|
|
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.
|
|
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
|
|
2475
|
-
"true",
|
|
2476
|
-
"false"
|
|
2477
|
-
]).pipe(
|
|
2664
|
+
var commitSetupOption = Options.choice("commit-setup", ["true", "false"]).pipe(
|
|
2478
2665
|
Options.withDescription(
|
|
2479
|
-
"Whether to
|
|
2480
|
-
)
|
|
2481
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
const
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
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
|
-
|
|
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:
|
|
2758
|
+
message: `${flag} is required in non-interactive mode (no TTY detected).`
|
|
2585
2759
|
})
|
|
2586
2760
|
);
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
);
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
{
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
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
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
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
|
-
|
|
2760
|
-
|
|
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
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
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
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
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
|
-
|
|
2804
|
-
|
|
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
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
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
|
-
|
|
2813
|
-
|
|
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
|
-
|
|
2820
|
-
const
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
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
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
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
|
-
|
|
3163
|
+
"No Shipyard installation found in this repository.",
|
|
2838
3164
|
"info"
|
|
2839
3165
|
);
|
|
3166
|
+
return;
|
|
2840
3167
|
}
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
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 (
|
|
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
|
-
`
|
|
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
|
-
|
|
2885
|
-
"
|
|
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
|
-
`
|
|
3228
|
+
`Preserved ${REPOSITORY_RUNNER_WORKFLOW_PATH} because it is customized or linked.`,
|
|
2889
3229
|
"warn"
|
|
2890
3230
|
);
|
|
2891
3231
|
}
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
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*
|
|
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
|