@cimplify/sdk 0.44.33 → 0.44.34

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.
@@ -0,0 +1,584 @@
1
+ import { promises } from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import readline from 'readline';
5
+
6
+ // src/cli/errors.ts
7
+ var CLI_ERROR_CODE = {
8
+ NOT_LOGGED_IN: "NOT_LOGGED_IN",
9
+ NOT_LINKED: "NOT_LINKED",
10
+ NETWORK_ERROR: "NETWORK_ERROR",
11
+ AUTH_FAILED: "AUTH_FAILED",
12
+ PROJECT_NOT_FOUND: "PROJECT_NOT_FOUND",
13
+ INVALID_INPUT: "INVALID_INPUT",
14
+ ALREADY_LINKED: "ALREADY_LINKED",
15
+ SERVER_ERROR: "SERVER_ERROR",
16
+ ABORTED: "ABORTED",
17
+ /** Operation needs interactive confirmation and the shell is non-interactive. */
18
+ INTERACTIVE_REQUIRED: "INTERACTIVE_REQUIRED"
19
+ };
20
+ var EXIT_CODE = {
21
+ ABORTED: 3,
22
+ NOT_LINKED: 4,
23
+ ALREADY_LINKED: 5,
24
+ GIT_ERROR: 6,
25
+ INTERACTIVE_REQUIRED: 7,
26
+ PROJECT_NOT_FOUND: 8,
27
+ NETWORK_ERROR: 10,
28
+ SERVER_ERROR: 11,
29
+ TIMEOUT: 12,
30
+ NOT_LOGGED_IN: 20,
31
+ AUTH_FAILED: 21,
32
+ UNAUTHORIZED: 22,
33
+ INVALID_INPUT: 30
34
+ };
35
+ var EXIT_CODE_FOR = {
36
+ NOT_LOGGED_IN: EXIT_CODE.NOT_LOGGED_IN,
37
+ NOT_LINKED: EXIT_CODE.NOT_LINKED,
38
+ NETWORK_ERROR: EXIT_CODE.NETWORK_ERROR,
39
+ AUTH_FAILED: EXIT_CODE.AUTH_FAILED,
40
+ PROJECT_NOT_FOUND: EXIT_CODE.PROJECT_NOT_FOUND,
41
+ GIT_ERROR: EXIT_CODE.GIT_ERROR,
42
+ INVALID_INPUT: EXIT_CODE.INVALID_INPUT,
43
+ ALREADY_LINKED: EXIT_CODE.ALREADY_LINKED,
44
+ SERVER_ERROR: EXIT_CODE.SERVER_ERROR,
45
+ ABORTED: EXIT_CODE.ABORTED,
46
+ UNAUTHORIZED: EXIT_CODE.UNAUTHORIZED,
47
+ TIMEOUT: EXIT_CODE.TIMEOUT,
48
+ INTERACTIVE_REQUIRED: EXIT_CODE.INTERACTIVE_REQUIRED
49
+ };
50
+ var CliError = class extends Error {
51
+ constructor(code, message, options = {}) {
52
+ super(message);
53
+ this.name = "CliError";
54
+ this.code = code;
55
+ this.exitCode = options.exitCode ?? EXIT_CODE_FOR[code];
56
+ this.remediation = options.remediation;
57
+ }
58
+ };
59
+
60
+ // src/cli/api-client.ts
61
+ var DEFAULT_API_BASE_URL = "https://api.cimplify.io";
62
+ var ENV_API_BASE_URL = "CIMPLIFY_API_URL";
63
+ var DEFAULT_TIMEOUT_MS = 3e4;
64
+ var HEADER_AUTHORIZATION = "Authorization";
65
+ var HEADER_CONTENT_TYPE = "Content-Type";
66
+ var HEADER_ACCEPT = "Accept";
67
+ var HEADER_USER_AGENT = "User-Agent";
68
+ var BEARER_PREFIX = "Bearer ";
69
+ var CONTENT_TYPE_JSON = "application/json";
70
+ var USER_AGENT = "cimplify-cli";
71
+ var METHOD_GET = "GET";
72
+ var METHOD_POST = "POST";
73
+ var METHOD_PUT = "PUT";
74
+ var METHOD_DELETE = "DELETE";
75
+ var STATUS_UNAUTHORIZED = 401;
76
+ var STATUS_FORBIDDEN = 403;
77
+ var STATUS_NOT_FOUND = 404;
78
+ var STATUS_CONFLICT = 409;
79
+ var STATUS_BAD_REQUEST_MIN = 400;
80
+ var STATUS_SERVER_ERROR_MIN = 500;
81
+ var ApiClient = class _ApiClient {
82
+ constructor(baseUrl, apiKey) {
83
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
84
+ this.apiKey = apiKey;
85
+ }
86
+ static fromAuth(auth) {
87
+ return new _ApiClient(auth.apiBaseUrl, auth.apiKey);
88
+ }
89
+ static unauthenticated(baseUrl = resolveBaseUrl()) {
90
+ return new _ApiClient(baseUrl, "");
91
+ }
92
+ static withKey(apiKey, baseUrl = resolveBaseUrl()) {
93
+ return new _ApiClient(baseUrl, apiKey);
94
+ }
95
+ get(pathname, options) {
96
+ return this.request(METHOD_GET, pathname, void 0, options);
97
+ }
98
+ post(pathname, body, options) {
99
+ return this.request(METHOD_POST, pathname, body, options);
100
+ }
101
+ put(pathname, body, options) {
102
+ return this.request(METHOD_PUT, pathname, body, options);
103
+ }
104
+ delete(pathname, options) {
105
+ return this.request(METHOD_DELETE, pathname, void 0, options);
106
+ }
107
+ async request(method, pathname, body, options) {
108
+ const url = this.buildUrl(pathname, options?.query);
109
+ const headers = {
110
+ [HEADER_ACCEPT]: CONTENT_TYPE_JSON,
111
+ [HEADER_USER_AGENT]: USER_AGENT
112
+ };
113
+ if (this.apiKey) {
114
+ headers[HEADER_AUTHORIZATION] = `${BEARER_PREFIX}${this.apiKey}`;
115
+ }
116
+ let payload;
117
+ if (body !== void 0) {
118
+ headers[HEADER_CONTENT_TYPE] = CONTENT_TYPE_JSON;
119
+ payload = JSON.stringify(body);
120
+ }
121
+ const controller = new AbortController();
122
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
123
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
124
+ let response;
125
+ try {
126
+ response = await fetch(url, {
127
+ method,
128
+ headers,
129
+ body: payload,
130
+ signal: controller.signal
131
+ });
132
+ } catch (err) {
133
+ clearTimeout(timer);
134
+ throw toNetworkError(err);
135
+ }
136
+ clearTimeout(timer);
137
+ return parseResponse(response);
138
+ }
139
+ buildUrl(pathname, query) {
140
+ const base = pathname.startsWith("/") ? pathname : `/${pathname}`;
141
+ const url = new URL(`${this.baseUrl}${base}`);
142
+ if (query) {
143
+ for (const [k, v] of Object.entries(query)) {
144
+ if (v === void 0 || v === null) continue;
145
+ url.searchParams.set(k, String(v));
146
+ }
147
+ }
148
+ return url.toString();
149
+ }
150
+ };
151
+ function resolveBaseUrl(override) {
152
+ const fromEnv = process.env[ENV_API_BASE_URL];
153
+ return fromEnv && fromEnv.length > 0 ? fromEnv : DEFAULT_API_BASE_URL;
154
+ }
155
+ async function parseResponse(response) {
156
+ const text = await response.text();
157
+ let body = void 0;
158
+ if (text.length > 0) {
159
+ try {
160
+ body = JSON.parse(text);
161
+ } catch {
162
+ throw new CliError(
163
+ CLI_ERROR_CODE.SERVER_ERROR,
164
+ `Server returned non-JSON response (status ${response.status}): ${truncate(text)}`
165
+ );
166
+ }
167
+ }
168
+ if (response.ok) {
169
+ if (body && typeof body === "object" && "data" in body) {
170
+ return body.data;
171
+ }
172
+ return body;
173
+ }
174
+ const code = mapStatusToCode(response.status);
175
+ const message = extractErrorMessage(body, response.status);
176
+ throw new CliError(code, message);
177
+ }
178
+ function mapStatusToCode(status) {
179
+ if (status === STATUS_UNAUTHORIZED) return CLI_ERROR_CODE.AUTH_FAILED;
180
+ if (status === STATUS_FORBIDDEN) return CLI_ERROR_CODE.AUTH_FAILED;
181
+ if (status === STATUS_NOT_FOUND) return CLI_ERROR_CODE.PROJECT_NOT_FOUND;
182
+ if (status === STATUS_CONFLICT) return CLI_ERROR_CODE.ALREADY_LINKED;
183
+ if (status >= STATUS_SERVER_ERROR_MIN) return CLI_ERROR_CODE.SERVER_ERROR;
184
+ if (status >= STATUS_BAD_REQUEST_MIN) return CLI_ERROR_CODE.INVALID_INPUT;
185
+ return CLI_ERROR_CODE.SERVER_ERROR;
186
+ }
187
+ function extractErrorMessage(body, status) {
188
+ if (body && typeof body === "object") {
189
+ const envelope = body;
190
+ if (envelope.error && typeof envelope.error.error_message === "string") {
191
+ return envelope.error.error_code ? `${envelope.error.error_code}: ${envelope.error.error_message}` : envelope.error.error_message;
192
+ }
193
+ if (typeof envelope.message === "string") return envelope.message;
194
+ }
195
+ return `HTTP ${status}`;
196
+ }
197
+ function toNetworkError(err) {
198
+ if (err instanceof DOMException && err.name === "AbortError") {
199
+ return new CliError(CLI_ERROR_CODE.NETWORK_ERROR, "Request timed out.");
200
+ }
201
+ const message = err instanceof Error ? err.message : String(err);
202
+ return new CliError(CLI_ERROR_CODE.NETWORK_ERROR, `Network error: ${message}`);
203
+ }
204
+ function truncate(value, max = 200) {
205
+ return value.length > max ? `${value.slice(0, max)}\u2026` : value;
206
+ }
207
+
208
+ // src/cli/args.ts
209
+ function parseArgs(argv) {
210
+ const positional = [];
211
+ const flags = /* @__PURE__ */ new Map();
212
+ for (let i = 0; i < argv.length; i++) {
213
+ const arg = argv[i];
214
+ if (!arg.startsWith("--")) {
215
+ positional.push(arg);
216
+ continue;
217
+ }
218
+ const eq = arg.indexOf("=");
219
+ if (eq !== -1) {
220
+ flags.set(arg.slice(2, eq), arg.slice(eq + 1));
221
+ continue;
222
+ }
223
+ const name = arg.slice(2);
224
+ const next = argv[i + 1];
225
+ if (next !== void 0 && !next.startsWith("--")) {
226
+ flags.set(name, next);
227
+ i++;
228
+ } else {
229
+ flags.set(name, true);
230
+ }
231
+ }
232
+ return { positional, flags };
233
+ }
234
+ function flagString(args, name) {
235
+ const v = args.flags.get(name);
236
+ return typeof v === "string" ? v : void 0;
237
+ }
238
+ function flagBool(args, name) {
239
+ return args.flags.has(name);
240
+ }
241
+ var AUTH_FILE_NAME = "auth.json";
242
+ var PROJECT_FILE_NAME = "project.json";
243
+ var PROJECT_DIR_NAME = ".cimplify";
244
+ var ENV_XDG_CONFIG_HOME = "XDG_CONFIG_HOME";
245
+ var APP_CONFIG_DIR_NAME = "cimplify";
246
+ var HOME_CONFIG_DIR_NAME = ".config";
247
+ var ENCODING_UTF8 = "utf8";
248
+ function authConfigDir() {
249
+ const xdg = process.env[ENV_XDG_CONFIG_HOME];
250
+ return xdg ? path.join(xdg, APP_CONFIG_DIR_NAME) : path.join(os.homedir(), HOME_CONFIG_DIR_NAME, APP_CONFIG_DIR_NAME);
251
+ }
252
+ function authConfigPath() {
253
+ return path.join(authConfigDir(), AUTH_FILE_NAME);
254
+ }
255
+ function projectConfigDir(cwd = process.cwd()) {
256
+ return path.join(cwd, PROJECT_DIR_NAME);
257
+ }
258
+ function projectConfigPath(cwd = process.cwd()) {
259
+ return path.join(projectConfigDir(cwd), PROJECT_FILE_NAME);
260
+ }
261
+ async function readJsonFile(filePath) {
262
+ try {
263
+ const raw = await promises.readFile(filePath, ENCODING_UTF8);
264
+ return JSON.parse(raw);
265
+ } catch (err) {
266
+ if (err.code === "ENOENT") return null;
267
+ throw err;
268
+ }
269
+ }
270
+ async function readAuth() {
271
+ const data = await readJsonFile(authConfigPath());
272
+ if (!data) {
273
+ throw new CliError(
274
+ CLI_ERROR_CODE.NOT_LOGGED_IN,
275
+ "Not logged in. Run `cimplify login` first."
276
+ );
277
+ }
278
+ return data;
279
+ }
280
+ async function readProjectLink(cwd = process.cwd()) {
281
+ const data = await readJsonFile(projectConfigPath(cwd));
282
+ if (!data) {
283
+ throw new CliError(
284
+ CLI_ERROR_CODE.NOT_LINKED,
285
+ "No project linked. Run `cimplify link <project-id>` first."
286
+ );
287
+ }
288
+ return data;
289
+ }
290
+
291
+ // src/cli/output.ts
292
+ var RESET = "\x1B[0m";
293
+ var BOLD_OPEN = "\x1B[1m";
294
+ var DIM_OPEN = "\x1B[2m";
295
+ var GREEN_OPEN = "\x1B[32m";
296
+ var PREFIX_SUCCESS = "\u2713";
297
+ var ENV_JSON = "CIMPLIFY_JSON";
298
+ var ENV_YES = "CIMPLIFY_YES";
299
+ function envFlag(name) {
300
+ const v = process.env[name];
301
+ return v === "1" || v === "true";
302
+ }
303
+ function isJsonMode() {
304
+ return envFlag(ENV_JSON);
305
+ }
306
+ function isAutoYes() {
307
+ return envFlag(ENV_YES);
308
+ }
309
+ function isInteractive() {
310
+ return Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY);
311
+ }
312
+ function wrap(open, value) {
313
+ return `${open}${value}${RESET}`;
314
+ }
315
+ function bold(value) {
316
+ return isJsonMode() ? value : wrap(BOLD_OPEN, value);
317
+ }
318
+ function dim(value) {
319
+ return isJsonMode() ? value : wrap(DIM_OPEN, value);
320
+ }
321
+ function green(value) {
322
+ return isJsonMode() ? value : wrap(GREEN_OPEN, value);
323
+ }
324
+ function success(message) {
325
+ if (isJsonMode()) return;
326
+ console.log(`${green(PREFIX_SUCCESS)} ${message}`);
327
+ }
328
+ function info(message) {
329
+ if (isJsonMode()) return;
330
+ console.log(message);
331
+ }
332
+ var ENV_EMITTED = "__CIMPLIFY_RESULT_EMITTED";
333
+ function result(data) {
334
+ if (!isJsonMode()) return;
335
+ if (process.env[ENV_EMITTED] === "1") return;
336
+ process.env[ENV_EMITTED] = "1";
337
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
338
+ `);
339
+ }
340
+ async function promptLine(question) {
341
+ if (!isInteractive()) {
342
+ throw new CliError(
343
+ CLI_ERROR_CODE.INTERACTIVE_REQUIRED,
344
+ "this operation needs interactive input but stdin is not a TTY",
345
+ { remediation: "run interactively, or supply the value via a flag" }
346
+ );
347
+ }
348
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
349
+ try {
350
+ return await new Promise((resolve) => {
351
+ rl.question(question, (answer) => resolve(answer));
352
+ });
353
+ } finally {
354
+ rl.close();
355
+ }
356
+ }
357
+ async function promptYesNo(question, defaultNo = true) {
358
+ if (isAutoYes()) return true;
359
+ if (!isInteractive()) {
360
+ throw new CliError(
361
+ CLI_ERROR_CODE.INTERACTIVE_REQUIRED,
362
+ `this operation needs confirmation: ${question}`,
363
+ { remediation: "re-run with --yes to accept, or run interactively" }
364
+ );
365
+ }
366
+ const suffix = defaultNo ? " [y/N] " : " [Y/n] ";
367
+ const answer = (await promptLine(`${question}${suffix}`)).trim().toLowerCase();
368
+ if (answer === "") return !defaultNo;
369
+ return answer === "y" || answer === "yes";
370
+ }
371
+ var REPO_PROVIDER = {
372
+ FREESTYLE: "freestyle",
373
+ GITHUB: "github",
374
+ GITEA: "gitea",
375
+ EXTERNAL: "external"
376
+ };
377
+ var REPO_PROVIDER_VALUES = new Set(Object.values(REPO_PROVIDER));
378
+ var TOKEN_PURPOSE = {
379
+ EDITOR: "editor"
380
+ };
381
+
382
+ // src/cli/commands/repo.ts
383
+ var SUB_PROVISION = "provision";
384
+ var SUB_CONNECT = "connect";
385
+ var SUB_INFO = "info";
386
+ var SUB_UNLINK = "unlink";
387
+ var SUB_ROTATE_TOKEN = "rotate-token";
388
+ var SUB_CLONE_URL = "clone-url";
389
+ var FLAG_PURGE = "purge";
390
+ var FLAG_YES = "yes";
391
+ var FLAG_PROVIDER = "provider";
392
+ var FLAG_NAME = "name";
393
+ var FLAG_BRANCH = "branch";
394
+ function repoEndpoint(businessId, projectId) {
395
+ return `/v1/businesses/${encodeURIComponent(businessId)}/projects/${encodeURIComponent(projectId)}/repo`;
396
+ }
397
+ function provisionEndpoint(businessId, projectId) {
398
+ return `${repoEndpoint(businessId, projectId)}/provision`;
399
+ }
400
+ function connectEndpoint(businessId, projectId) {
401
+ return `${repoEndpoint(businessId, projectId)}/connect`;
402
+ }
403
+ function cloneTokenEndpoint(businessId, projectId) {
404
+ return `${repoEndpoint(businessId, projectId)}/clone-token`;
405
+ }
406
+ async function fetchCloneToken(client, businessId, projectId, purpose = TOKEN_PURPOSE.EDITOR) {
407
+ return client.post(cloneTokenEndpoint(businessId, projectId), {
408
+ purpose
409
+ });
410
+ }
411
+ function resolveProvider(value, fallback) {
412
+ if (value === void 0) return fallback;
413
+ if (!REPO_PROVIDER_VALUES.has(value)) {
414
+ throw new CliError(
415
+ CLI_ERROR_CODE.INVALID_INPUT,
416
+ `Invalid provider "${value}". Choose: ${[...REPO_PROVIDER_VALUES].join(", ")}.`
417
+ );
418
+ }
419
+ return value;
420
+ }
421
+ function detectProviderFromUrl(url) {
422
+ try {
423
+ const parsed = new URL(url);
424
+ if (parsed.hostname === "git.freestyle.sh") return REPO_PROVIDER.FREESTYLE;
425
+ if (parsed.hostname === "github.com" || parsed.hostname.endsWith(".github.com")) {
426
+ return REPO_PROVIDER.GITHUB;
427
+ }
428
+ if (parsed.hostname.includes("gitea")) return REPO_PROVIDER.GITEA;
429
+ } catch {
430
+ }
431
+ return REPO_PROVIDER.EXTERNAL;
432
+ }
433
+ async function run(argv) {
434
+ const args = parseArgs(argv);
435
+ const sub = args.positional[0];
436
+ if (!sub) {
437
+ throw new CliError(
438
+ CLI_ERROR_CODE.INVALID_INPUT,
439
+ `Usage: cimplify repo <${SUB_PROVISION}|${SUB_CONNECT}|${SUB_INFO}|${SUB_UNLINK}|${SUB_ROTATE_TOKEN}|${SUB_CLONE_URL}>`
440
+ );
441
+ }
442
+ const auth = await readAuth();
443
+ const link = await readProjectLink();
444
+ const client = ApiClient.fromAuth(auth);
445
+ switch (sub) {
446
+ case SUB_PROVISION:
447
+ await provisionRepo(client, link.businessId, link.projectId, args);
448
+ return;
449
+ case SUB_CONNECT:
450
+ await connectRepo(client, link.businessId, link.projectId, args);
451
+ return;
452
+ case SUB_INFO:
453
+ await repoInfo(client, link.businessId, link.projectId);
454
+ return;
455
+ case SUB_UNLINK:
456
+ await unlinkRepo(client, link.businessId, link.projectId, args);
457
+ return;
458
+ case SUB_ROTATE_TOKEN:
459
+ await provisionRepo(client, link.businessId, link.projectId, args);
460
+ return;
461
+ case SUB_CLONE_URL:
462
+ await printCloneUrl(client, link.businessId, link.projectId);
463
+ return;
464
+ default:
465
+ throw new CliError(CLI_ERROR_CODE.INVALID_INPUT, `Unknown repo subcommand "${sub}"`);
466
+ }
467
+ }
468
+ async function printCloneUrl(client, businessId, projectId) {
469
+ const token = await fetchCloneToken(client, businessId, projectId);
470
+ if (isJsonMode()) {
471
+ result({
472
+ clone_url: token.clone_url,
473
+ token: token.token ?? null,
474
+ repo_id: token.repo_id,
475
+ provider: token.provider,
476
+ expires_in_seconds: token.expires_in_seconds
477
+ });
478
+ return;
479
+ }
480
+ process.stdout.write(`${token.clone_url}
481
+ `);
482
+ const hints = [
483
+ "",
484
+ dim(`Token TTL: ${token.expires_in_seconds}s. Re-run to mint a fresh one.`),
485
+ dim("Example:"),
486
+ dim(` git remote set-url origin "${token.clone_url}"`),
487
+ dim(" # or one-shot:"),
488
+ dim(` git push "${token.clone_url}" main`)
489
+ ];
490
+ for (const line of hints) process.stderr.write(`${line}
491
+ `);
492
+ }
493
+ async function provisionRepo(client, businessId, projectId, args) {
494
+ const provider = resolveProvider(flagString(args, FLAG_PROVIDER), REPO_PROVIDER.FREESTYLE);
495
+ if (provider !== REPO_PROVIDER.FREESTYLE) {
496
+ throw new CliError(
497
+ CLI_ERROR_CODE.INVALID_INPUT,
498
+ `Provisioning is only supported for ${REPO_PROVIDER.FREESTYLE} today. Use \`cimplify repo connect\` for ${provider}.`
499
+ );
500
+ }
501
+ const body = {
502
+ provider,
503
+ name_hint: flagString(args, FLAG_NAME) ?? void 0,
504
+ default_branch: flagString(args, FLAG_BRANCH) ?? void 0
505
+ };
506
+ const repo = await client.post(provisionEndpoint(businessId, projectId), body);
507
+ success(`Provisioned ${repo.provider} repo`);
508
+ printRepo(repo);
509
+ result({ repo: repoPayload(repo) });
510
+ }
511
+ async function connectRepo(client, businessId, projectId, args) {
512
+ const remoteUrl = args.positional[1];
513
+ if (!remoteUrl) {
514
+ throw new CliError(
515
+ CLI_ERROR_CODE.INVALID_INPUT,
516
+ `Usage: cimplify repo ${SUB_CONNECT} <https-clone-url> [--provider <p>] [--branch <name>]`
517
+ );
518
+ }
519
+ const provider = resolveProvider(flagString(args, FLAG_PROVIDER), detectProviderFromUrl(remoteUrl));
520
+ if (provider === REPO_PROVIDER.FREESTYLE) {
521
+ throw new CliError(
522
+ CLI_ERROR_CODE.INVALID_INPUT,
523
+ "Cannot connect a Freestyle URL \u2014 use `cimplify repo provision` instead."
524
+ );
525
+ }
526
+ const body = {
527
+ provider,
528
+ remote_url: remoteUrl,
529
+ default_branch: flagString(args, FLAG_BRANCH) ?? void 0
530
+ };
531
+ const repo = await client.post(connectEndpoint(businessId, projectId), body);
532
+ success(`Connected ${repo.provider} remote ${repo.remote_url}`);
533
+ printRepo(repo);
534
+ result({ repo: repoPayload(repo) });
535
+ }
536
+ async function repoInfo(client, businessId, projectId) {
537
+ const repo = await client.get(repoEndpoint(businessId, projectId));
538
+ if (!repo) {
539
+ info(dim("No repo attached. Run `cimplify repo provision` to mint a Freestyle repo,"));
540
+ info(dim("or `cimplify repo connect <url>` to link an existing remote."));
541
+ result({ repo: null });
542
+ return;
543
+ }
544
+ printRepo(repo);
545
+ result({ repo: repoPayload(repo) });
546
+ }
547
+ async function unlinkRepo(client, businessId, projectId, args) {
548
+ const purge = flagBool(args, FLAG_PURGE);
549
+ const skipPrompt = flagBool(args, FLAG_YES);
550
+ if (!skipPrompt) {
551
+ const message = purge ? "Unlink and DELETE the upstream Freestyle repo? This cannot be undone." : "Unlink the repo from this project? (Upstream repo will be preserved.)";
552
+ const proceed = await promptYesNo(message, false);
553
+ if (!proceed) {
554
+ throw new CliError(CLI_ERROR_CODE.ABORTED, "Aborted by user.");
555
+ }
556
+ }
557
+ await client.delete(repoEndpoint(businessId, projectId), {
558
+ query: purge ? { purge: "true" } : void 0
559
+ });
560
+ success(purge ? "Repo unlinked and purged" : "Repo unlinked (upstream preserved)");
561
+ result({ unlinked: true, purged: purge });
562
+ }
563
+ function printRepo(repo) {
564
+ info(bold("Repo:"));
565
+ info(` provider: ${repo.provider}`);
566
+ info(` id: ${repo.id}`);
567
+ info(` remote_url: ${repo.remote_url}`);
568
+ info(` default_branch: ${repo.default_branch}`);
569
+ if (repo.last_pushed_commit) {
570
+ info(` last_pushed: ${repo.last_pushed_commit.slice(0, 12)} (${repo.last_pushed_at ?? "\u2014"})`);
571
+ }
572
+ }
573
+ function repoPayload(repo) {
574
+ return {
575
+ provider: repo.provider,
576
+ id: repo.id,
577
+ remote_url: repo.remote_url,
578
+ default_branch: repo.default_branch,
579
+ last_pushed_commit: repo.last_pushed_commit ?? null,
580
+ last_pushed_at: repo.last_pushed_at ?? null
581
+ };
582
+ }
583
+
584
+ export { run as default, fetchCloneToken };