offworld 0.1.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.
@@ -0,0 +1,2447 @@
1
+ import { existsSync, readFileSync, rmSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { os } from "@orpc/server";
4
+ import { createCli } from "trpc-cli";
5
+ import { z } from "zod";
6
+ import * as p from "@clack/prompts";
7
+ import { AuthenticationError, CommitExistsError, CommitNotFoundError, GitHubError, InvalidInputError, InvalidReferenceError, LowStarsError, NotLoggedInError, Paths, PrivateRepoError, RateLimitError, RepoExistsError, SyncRepoNotFoundError, TokenExpiredError, checkRemote, checkRemoteByName, cleanShellConfig, clearAuthData, cloneRepo, detectInstallMethod, detectInstalledAgents, discoverRepos, executeUninstall, executeUpgrade, fetchLatestVersion, gcRepos, generateReferenceWithAI, getAllAgentConfigs, getAuthPath, getAuthStatus, getClonedRepoPath, getCommitDistance, getCommitSha, getConfigPath, getCurrentVersion, getMapEntry, getMetaPath, getMetaRoot, getProvider, getReferencePath, getRepoRoot, getRepoStatus, getShellConfigFiles, getToken, installGlobalSkill, installReference, isRepoCloned, listProviders, listRepos, loadAuthData, loadConfig, matchDependenciesToReferences, parseDependencies, parseRepoInput, pruneRepos, pullReference, pullReferenceByName, pushReference, readGlobalMap, removeRepo, resolveDependencyRepo, saveAuthData, saveConfig, searchMap, toReferenceFileName, toReferenceName, updateAgentFiles, updateAllRepos, updateRepo, validateProviderModel, writeProjectMap } from "@offworld/sdk";
8
+ import { AgentSchema, ConfigSchema } from "@offworld/types/schemas";
9
+ import open from "open";
10
+ import { homedir } from "node:os";
11
+
12
+ //#region src/utils/spinner.ts
13
+ var NoOpSpinner = class {
14
+ start(_message) {}
15
+ stop(_message) {}
16
+ message(_message) {}
17
+ };
18
+ /**
19
+ * Creates a spinner that works in both TTY and non-TTY environments.
20
+ * In TTY: returns a real @clack/prompts spinner with animation
21
+ * In non-TTY: returns a no-op spinner that outputs nothing
22
+ *
23
+ * This prevents garbage output when running in non-interactive environments
24
+ * (CI, piped output, agent sessions).
25
+ */
26
+ function createSpinner() {
27
+ if (process.stdout.isTTY) return p.spinner();
28
+ return new NoOpSpinner();
29
+ }
30
+ /**
31
+ * Check if running in a TTY environment
32
+ */
33
+ const isTTY = process.stdout.isTTY ?? false;
34
+
35
+ //#endregion
36
+ //#region src/handlers/pull.ts
37
+ function timestamp() {
38
+ return (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
39
+ }
40
+ function verboseLog(message, verbose) {
41
+ if (verbose) p.log.info(`[${timestamp()}] ${message}`);
42
+ }
43
+ function getLocalMetaDir(source) {
44
+ return getMetaPath(source.type === "remote" ? source.fullName : source.name);
45
+ }
46
+ function loadLocalMeta(source) {
47
+ const metaPath = join(getLocalMetaDir(source), "meta.json");
48
+ if (!existsSync(metaPath)) return null;
49
+ try {
50
+ return JSON.parse(readFileSync(metaPath, "utf-8"));
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function hasValidCache(source, currentSha) {
56
+ return loadLocalMeta(source)?.commitSha?.slice(0, 7) === currentSha.slice(0, 7);
57
+ }
58
+ /**
59
+ * Save remote reference to local filesystem.
60
+ * Remote reference comes with pre-generated reference content.
61
+ */
62
+ function saveRemoteReference(qualifiedName, referenceRepoName, localPath, referenceContent, commitSha, referenceUpdatedAt) {
63
+ installReference(qualifiedName, referenceRepoName, localPath, referenceContent, {
64
+ referenceUpdatedAt,
65
+ commitSha,
66
+ version: "0.1.0"
67
+ });
68
+ }
69
+ function parseModelFlag$1(model) {
70
+ if (!model) return {};
71
+ const parts = model.split("/");
72
+ if (parts.length === 2) return {
73
+ provider: parts[0],
74
+ model: parts[1]
75
+ };
76
+ return { model };
77
+ }
78
+ async function pullHandler(options) {
79
+ const { repo, shallow = false, sparse = false, branch, force = false, verbose = false } = options;
80
+ const referenceName = options.reference?.trim() || void 0;
81
+ const { provider, model } = parseModelFlag$1(options.model);
82
+ const config = loadConfig();
83
+ const isReferenceOverride = Boolean(referenceName);
84
+ const s = createSpinner();
85
+ if (verbose) p.log.info(`[verbose] Options: repo=${repo}, reference=${referenceName ?? "default"}, shallow=${shallow}, branch=${branch || "default"}, force=${force}`);
86
+ try {
87
+ s.start("Parsing repository input...");
88
+ const source = parseRepoInput(repo);
89
+ s.stop("Repository parsed");
90
+ verboseLog(`Parsed source: type=${source.type}, qualifiedName=${source.qualifiedName}`, verbose);
91
+ let repoPath;
92
+ if (source.type === "remote") {
93
+ const qualifiedName = source.qualifiedName;
94
+ if (isRepoCloned(qualifiedName)) {
95
+ s.start("Updating repository...");
96
+ const result = await updateRepo(qualifiedName);
97
+ repoPath = getClonedRepoPath(qualifiedName);
98
+ if (result.updated) s.stop(`Updated (${result.previousSha.slice(0, 7)} → ${result.currentSha.slice(0, 7)})`);
99
+ else s.stop("Already up to date");
100
+ } else {
101
+ s.start(`Cloning ${source.fullName}...`);
102
+ try {
103
+ repoPath = await cloneRepo(source, {
104
+ shallow,
105
+ sparse,
106
+ branch,
107
+ config,
108
+ force
109
+ });
110
+ s.stop("Repository cloned");
111
+ verboseLog(`Cloned to: ${repoPath}`, verbose);
112
+ } catch (err) {
113
+ if (err instanceof RepoExistsError && !force) {
114
+ s.stop("Repository exists");
115
+ throw err;
116
+ }
117
+ throw err;
118
+ }
119
+ }
120
+ } else repoPath = source.path;
121
+ const currentSha = getCommitSha(repoPath);
122
+ const qualifiedName = source.type === "remote" ? source.fullName : source.name;
123
+ if (isReferenceOverride && source.type !== "remote") throw new Error("--reference can only be used with remote repositories");
124
+ if (isReferenceOverride && !referenceName) throw new Error("--reference requires a reference name");
125
+ const requiredReferenceName = referenceName ?? "";
126
+ if (!force && !isReferenceOverride && hasValidCache(source, currentSha)) {
127
+ verboseLog("Using cached reference", verbose);
128
+ s.stop("Using cached reference");
129
+ return {
130
+ success: true,
131
+ repoPath,
132
+ referenceSource: "cached",
133
+ referenceInstalled: true
134
+ };
135
+ }
136
+ if (source.type === "remote" && (!force || isReferenceOverride)) {
137
+ verboseLog(`Checking offworld.sh for reference: ${source.fullName}`, verbose);
138
+ s.start("Checking offworld.sh for reference...");
139
+ try {
140
+ const remoteCheck = isReferenceOverride ? await checkRemoteByName(source.fullName, requiredReferenceName) : await checkRemote(source.fullName);
141
+ if (remoteCheck.exists && remoteCheck.commitSha) {
142
+ const remoteSha = remoteCheck.commitSha;
143
+ const remoteShaNorm = remoteSha.slice(0, 7);
144
+ const currentShaNorm = currentSha.slice(0, 7);
145
+ const MAX_COMMIT_DISTANCE = 20;
146
+ const commitDistance = getCommitDistance(repoPath, remoteSha, currentSha);
147
+ if (isReferenceOverride || remoteShaNorm === currentShaNorm || commitDistance !== null && commitDistance <= MAX_COMMIT_DISTANCE) {
148
+ if (!isReferenceOverride) if (commitDistance === 0 || remoteShaNorm === currentShaNorm) {
149
+ verboseLog(`Remote SHA matches (${remoteShaNorm})`, verbose);
150
+ s.stop("Remote reference found (exact match)");
151
+ } else if (commitDistance !== null) {
152
+ verboseLog(`Remote reference is ${commitDistance} commits behind (within ${MAX_COMMIT_DISTANCE} threshold)`, verbose);
153
+ s.stop(`Remote reference found (${commitDistance} commits behind)`);
154
+ } else {
155
+ verboseLog("Remote reference found (commit distance unknown)", verbose);
156
+ s.stop("Remote reference found");
157
+ }
158
+ else s.stop("Remote reference found");
159
+ const previewUrl = referenceName ? `https://offworld.sh/${source.fullName}/${encodeURIComponent(referenceName)}` : `https://offworld.sh/${source.fullName}`;
160
+ p.log.info(`Preview: ${previewUrl}`);
161
+ const useRemote = await p.confirm({
162
+ message: "Download this reference from offworld.sh?",
163
+ initialValue: true
164
+ });
165
+ if (p.isCancel(useRemote)) throw new Error("Operation cancelled");
166
+ if (!useRemote) {
167
+ if (isReferenceOverride) throw new Error("Remote reference download declined");
168
+ p.log.info("Skipping remote reference, generating locally...");
169
+ } else {
170
+ s.start("Downloading remote reference...");
171
+ const remoteReference = isReferenceOverride ? await pullReferenceByName(source.fullName, requiredReferenceName) : await pullReference(source.fullName);
172
+ if (remoteReference) {
173
+ s.stop("Downloaded remote reference");
174
+ saveRemoteReference(source.qualifiedName, source.fullName, repoPath, remoteReference.referenceContent, remoteReference.commitSha, remoteReference.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString());
175
+ p.log.success(referenceName ? `Reference installed (${referenceName}) for: ${qualifiedName}` : `Reference installed for: ${qualifiedName}`);
176
+ return {
177
+ success: true,
178
+ repoPath,
179
+ referenceSource: "remote",
180
+ referenceInstalled: true
181
+ };
182
+ }
183
+ if (isReferenceOverride) throw new Error("Remote reference download failed");
184
+ s.stop("Remote download failed, generating locally...");
185
+ }
186
+ } else {
187
+ const distanceInfo = commitDistance !== null ? ` (${commitDistance} commits behind)` : "";
188
+ verboseLog(`Remote reference too outdated${distanceInfo}, threshold is ${MAX_COMMIT_DISTANCE}`, verbose);
189
+ s.stop(`Remote reference outdated${distanceInfo}`);
190
+ }
191
+ } else s.stop("No remote reference found");
192
+ } catch (err) {
193
+ verboseLog(`Remote check failed: ${err instanceof Error ? err.message : "Unknown"}`, verbose);
194
+ if (isReferenceOverride) throw err instanceof Error ? err : /* @__PURE__ */ new Error("Remote check failed");
195
+ s.stop("Remote check failed, continuing locally");
196
+ }
197
+ }
198
+ if (isReferenceOverride) throw new Error(`Reference not found on offworld.sh: ${referenceName}`);
199
+ verboseLog(`Starting AI reference generation for: ${repoPath}`, verbose);
200
+ if (!verbose) s.start("Generating reference with AI...");
201
+ try {
202
+ const { referenceContent, commitSha: referenceCommitSha } = await generateReferenceWithAI(repoPath, qualifiedName, {
203
+ provider,
204
+ model,
205
+ onDebug: verbose ? (message) => {
206
+ p.log.info(`[${timestamp()}] [debug] ${message}`);
207
+ } : (msg) => s.message(msg)
208
+ });
209
+ const meta = {
210
+ referenceUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(),
211
+ commitSha: referenceCommitSha,
212
+ version: "0.1.0"
213
+ };
214
+ const referenceRepoName = source.type === "remote" ? source.fullName : source.name;
215
+ installReference(source.qualifiedName, referenceRepoName, repoPath, referenceContent, meta);
216
+ if (!verbose) s.stop("Reference generated");
217
+ else p.log.success("Reference generated");
218
+ p.log.success(`Reference installed for: ${qualifiedName}`);
219
+ if (source.type === "remote") {
220
+ if (loadAuthData()?.token) p.log.info(`Run 'ow push ${source.fullName}' to share this reference to https://offworld.sh.`);
221
+ }
222
+ return {
223
+ success: true,
224
+ repoPath,
225
+ referenceSource: "local",
226
+ referenceInstalled: true
227
+ };
228
+ } catch (err) {
229
+ if (!verbose) s.stop("Reference generation failed");
230
+ const errMessage = err instanceof Error ? err.message : "Unknown error";
231
+ p.log.error(`Failed to generate reference: ${errMessage}`);
232
+ throw new Error(`Reference generation failed: ${errMessage}`);
233
+ }
234
+ } catch (error) {
235
+ s.stop("Failed");
236
+ const message = error instanceof Error ? error.message : "Unknown error";
237
+ p.log.error(message);
238
+ return {
239
+ success: false,
240
+ repoPath: "",
241
+ referenceSource: "local",
242
+ referenceInstalled: false,
243
+ message
244
+ };
245
+ }
246
+ }
247
+
248
+ //#endregion
249
+ //#region src/handlers/generate.ts
250
+ function parseModelFlag(model) {
251
+ if (!model) return {};
252
+ const parts = model.split("/");
253
+ if (parts.length === 2) return {
254
+ provider: parts[0],
255
+ model: parts[1]
256
+ };
257
+ return { model };
258
+ }
259
+ async function generateHandler(options) {
260
+ const { repo, force = false } = options;
261
+ const { provider, model } = parseModelFlag(options.model);
262
+ const config = loadConfig();
263
+ const s = createSpinner();
264
+ try {
265
+ s.start("Parsing repository input...");
266
+ const source = parseRepoInput(repo);
267
+ s.stop("Repository parsed");
268
+ let repoPath;
269
+ if (source.type === "remote" && !force) {
270
+ s.start("Checking for existing remote reference...");
271
+ if ((await checkRemote(source.fullName)).exists) {
272
+ s.stop("Remote reference exists");
273
+ return {
274
+ success: false,
275
+ message: "Remote reference exists. Use --force to override."
276
+ };
277
+ }
278
+ s.stop("No remote reference found");
279
+ }
280
+ if (source.type === "remote") {
281
+ const qualifiedName = source.qualifiedName;
282
+ if (isRepoCloned(qualifiedName)) {
283
+ repoPath = getClonedRepoPath(qualifiedName);
284
+ p.log.info(`Using existing clone at ${repoPath}`);
285
+ } else {
286
+ s.start(`Cloning ${source.fullName}...`);
287
+ repoPath = await cloneRepo(source, {
288
+ shallow: config.defaultShallow,
289
+ config
290
+ });
291
+ s.stop("Repository cloned");
292
+ }
293
+ } else repoPath = source.path;
294
+ s.start("Generating reference with AI...");
295
+ const qualifiedName = source.qualifiedName;
296
+ const referenceRepoName = source.type === "remote" ? source.fullName : source.name;
297
+ const result = await generateReferenceWithAI(repoPath, referenceRepoName, {
298
+ provider,
299
+ model,
300
+ onDebug: (msg) => s.message(msg)
301
+ });
302
+ s.stop("Reference generated");
303
+ const { referenceContent, commitSha } = result;
304
+ const meta = {
305
+ referenceUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(),
306
+ commitSha,
307
+ version: "0.1.0"
308
+ };
309
+ const referencePath = getReferencePath(referenceRepoName);
310
+ installReference(qualifiedName, referenceRepoName, repoPath, referenceContent, meta);
311
+ p.log.success(`Reference saved to: ${referencePath}`);
312
+ p.log.info(`Reference installed for: ${qualifiedName}`);
313
+ return {
314
+ success: true,
315
+ referencePath
316
+ };
317
+ } catch (error) {
318
+ s.stop("Failed");
319
+ const message = error instanceof Error ? error.message : "Unknown error";
320
+ p.log.error(message);
321
+ return {
322
+ success: false,
323
+ message
324
+ };
325
+ }
326
+ }
327
+
328
+ //#endregion
329
+ //#region src/handlers/shared.ts
330
+ /**
331
+ * Format a repo entry for display
332
+ */
333
+ function formatRepoForDisplay(item, showPaths) {
334
+ const parts = [item.fullName];
335
+ if (item.hasReference) parts.push("[reference]");
336
+ else parts.push("[no-reference]");
337
+ if (showPaths) parts.push(`(${item.localPath})`);
338
+ if (!item.exists) parts.push("[missing]");
339
+ return parts.join(" ");
340
+ }
341
+
342
+ //#endregion
343
+ //#region src/handlers/repo.ts
344
+ function formatBytes(bytes) {
345
+ if (bytes < 1024) return `${bytes} B`;
346
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
347
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
348
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
349
+ }
350
+ function parseDays(olderThan) {
351
+ const match = olderThan.match(/^(\d+)d$/);
352
+ if (match?.[1]) return parseInt(match[1], 10);
353
+ return null;
354
+ }
355
+ function matchesPattern(name, pattern) {
356
+ if (!pattern || pattern === "*") return true;
357
+ return new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$", "i").test(name);
358
+ }
359
+ async function repoListHandler(options) {
360
+ const { json = false, paths = false, pattern } = options;
361
+ const qualifiedNames = listRepos();
362
+ const map = readGlobalMap();
363
+ if (qualifiedNames.length === 0) {
364
+ if (!json) {
365
+ p.log.info("No repositories cloned yet.");
366
+ p.log.info("Use 'ow pull <repo>' to clone and generate a reference.");
367
+ }
368
+ return { repos: [] };
369
+ }
370
+ const items = [];
371
+ for (const qName of qualifiedNames) {
372
+ const entry = map.repos[qName];
373
+ if (!entry) continue;
374
+ if (pattern && !matchesPattern(qName, pattern)) continue;
375
+ const exists = existsSync(entry.localPath);
376
+ const hasReference = !!entry.primary;
377
+ items.push({
378
+ fullName: qName,
379
+ qualifiedName: qName,
380
+ localPath: entry.localPath,
381
+ hasReference,
382
+ referenceUpdatedAt: entry.updatedAt,
383
+ exists
384
+ });
385
+ }
386
+ if (json) console.log(JSON.stringify(items, null, 2));
387
+ else if (items.length === 0) {
388
+ if (pattern) p.log.info(`No repositories matching "${pattern}".`);
389
+ } else {
390
+ p.log.info(`Found ${items.length} repositories:\n`);
391
+ for (const item of items) console.log(formatRepoForDisplay(item, paths));
392
+ }
393
+ return { repos: items };
394
+ }
395
+ async function repoUpdateHandler(options) {
396
+ const { all = false, pattern, dryRun = false, unshallow = false } = options;
397
+ if (!all && !pattern) {
398
+ p.log.error("Specify --all or a pattern to update.");
399
+ return {
400
+ updated: [],
401
+ skipped: [],
402
+ unshallowed: [],
403
+ errors: []
404
+ };
405
+ }
406
+ const qualifiedNames = listRepos();
407
+ const total = (pattern ? qualifiedNames.filter((q) => matchesPattern(q, pattern)) : qualifiedNames).length;
408
+ if (total === 0) {
409
+ p.log.info(pattern ? `No repos matching "${pattern}"` : "No repos to update");
410
+ return {
411
+ updated: [],
412
+ skipped: [],
413
+ unshallowed: [],
414
+ errors: []
415
+ };
416
+ }
417
+ let processed = 0;
418
+ const outcomes = [];
419
+ const spinner = p.spinner();
420
+ const action = unshallow ? "Unshallowing" : "Updating";
421
+ spinner.start(dryRun ? `Checking ${total} repos...` : `${action} ${total} repos...`);
422
+ const result = await updateAllRepos({
423
+ pattern,
424
+ dryRun,
425
+ unshallow,
426
+ onProgress: (repo, status, message) => {
427
+ if (status === "updating") {
428
+ processed++;
429
+ spinner.message(`[${processed}/${total}] ${repo}`);
430
+ } else outcomes.push({
431
+ repo,
432
+ status,
433
+ message
434
+ });
435
+ }
436
+ });
437
+ spinner.stop(dryRun ? "Dry run complete" : `${action} complete`);
438
+ for (const { repo, status, message } of outcomes) if (status === "updated") p.log.success(`${repo}${message ? ` (${message})` : ""}`);
439
+ else if (status === "unshallowed") p.log.success(`${repo}: ${message}`);
440
+ else if (status === "error") p.log.error(`${repo}: ${message}`);
441
+ else if (status === "skipped" && message !== "up to date" && message !== "already up to date") p.log.warn(`${repo}: ${message}`);
442
+ const parts = [];
443
+ if (result.updated.length > 0) parts.push(`${result.updated.length} ${dryRun ? "would update" : "updated"}`);
444
+ if (result.unshallowed.length > 0) parts.push(`${result.unshallowed.length} unshallowed`);
445
+ if (result.skipped.length > 0) parts.push(`${result.skipped.length} skipped`);
446
+ if (result.errors.length > 0) parts.push(`${result.errors.length} failed`);
447
+ if (parts.length > 0) p.log.info(`Summary: ${parts.join(", ")}`);
448
+ return result;
449
+ }
450
+ async function repoPruneHandler(options) {
451
+ const { dryRun = false, yes = false, removeOrphans = false } = options;
452
+ const result = await pruneRepos({ dryRun: true });
453
+ const removedOrphans = [];
454
+ if (result.removedFromIndex.length === 0 && result.orphanedDirs.length === 0) {
455
+ p.log.info("Nothing to prune. Index and filesystem are in sync.");
456
+ return {
457
+ removedFromIndex: [],
458
+ orphanedDirs: [],
459
+ removedOrphans: []
460
+ };
461
+ }
462
+ if (result.removedFromIndex.length > 0) {
463
+ p.log.info(`Found ${result.removedFromIndex.length} repos in index but missing on disk:`);
464
+ for (const repo of result.removedFromIndex) console.log(` - ${repo}`);
465
+ }
466
+ if (result.orphanedDirs.length > 0) {
467
+ p.log.info(`Found ${result.orphanedDirs.length} directories not in index:`);
468
+ for (const dir of result.orphanedDirs) console.log(` - ${dir}`);
469
+ }
470
+ if (dryRun) {
471
+ p.log.info("Dry run - no changes made.");
472
+ return {
473
+ ...result,
474
+ removedOrphans: []
475
+ };
476
+ }
477
+ let shouldProceed = yes;
478
+ if (!yes) {
479
+ const confirm = await p.confirm({ message: "Remove stale index entries?" });
480
+ if (p.isCancel(confirm)) {
481
+ p.log.info("Cancelled.");
482
+ return {
483
+ removedFromIndex: [],
484
+ orphanedDirs: result.orphanedDirs,
485
+ removedOrphans: []
486
+ };
487
+ }
488
+ shouldProceed = confirm;
489
+ }
490
+ if (shouldProceed) {
491
+ await pruneRepos({ dryRun: false });
492
+ p.log.success(`Removed ${result.removedFromIndex.length} stale index entries.`);
493
+ }
494
+ if (removeOrphans && result.orphanedDirs.length > 0) {
495
+ let shouldRemoveOrphans = yes;
496
+ if (!yes) {
497
+ const confirm = await p.confirm({ message: "Also remove orphaned directories?" });
498
+ if (!p.isCancel(confirm) && confirm) shouldRemoveOrphans = true;
499
+ }
500
+ if (shouldRemoveOrphans) {
501
+ for (const dir of result.orphanedDirs) {
502
+ rmSync(dir, {
503
+ recursive: true,
504
+ force: true
505
+ });
506
+ removedOrphans.push(dir);
507
+ }
508
+ p.log.success(`Removed ${removedOrphans.length} orphaned directories.`);
509
+ }
510
+ }
511
+ return {
512
+ ...result,
513
+ removedOrphans
514
+ };
515
+ }
516
+ async function repoStatusHandler(options) {
517
+ const { json = false } = options;
518
+ const spinner = p.spinner();
519
+ spinner.start("Calculating repo status...");
520
+ const status = await getRepoStatus({ onProgress: (current, total, repo) => {
521
+ spinner.message(`[${current}/${total}] ${repo}`);
522
+ } });
523
+ spinner.stop("Status complete");
524
+ const output = {
525
+ total: status.total,
526
+ withReference: status.withReference,
527
+ missing: status.missing,
528
+ diskMB: Math.round(status.diskBytes / (1024 * 1024))
529
+ };
530
+ if (json) console.log(JSON.stringify(output, null, 2));
531
+ else {
532
+ p.log.info(`Managed repos: ${status.total}`);
533
+ p.log.info(` With reference: ${status.withReference}`);
534
+ p.log.info(` Missing: ${status.missing}`);
535
+ p.log.info(` Disk usage: ${formatBytes(status.diskBytes)}`);
536
+ }
537
+ return output;
538
+ }
539
+ async function repoGcHandler(options) {
540
+ const { olderThan, withoutReference = false, dryRun = false, yes = false } = options;
541
+ if (!olderThan && !withoutReference) {
542
+ p.log.error("Specify at least one filter: --older-than or --without-reference");
543
+ return {
544
+ removed: [],
545
+ freedMB: 0
546
+ };
547
+ }
548
+ let olderThanDays;
549
+ if (olderThan) {
550
+ const days = parseDays(olderThan);
551
+ if (days === null) {
552
+ p.log.error("Invalid --older-than format. Use e.g. '30d' for 30 days.");
553
+ return {
554
+ removed: [],
555
+ freedMB: 0
556
+ };
557
+ }
558
+ olderThanDays = days;
559
+ }
560
+ const previewResult = await gcRepos({
561
+ olderThanDays,
562
+ withoutReference,
563
+ dryRun: true
564
+ });
565
+ if (previewResult.removed.length === 0) {
566
+ p.log.info("No repos match the criteria.");
567
+ return {
568
+ removed: [],
569
+ freedMB: 0
570
+ };
571
+ }
572
+ p.log.info(`Found ${previewResult.removed.length} repos to remove (${formatBytes(previewResult.freedBytes)}):`);
573
+ for (const { repo, reason, sizeBytes } of previewResult.removed) console.log(` - ${repo} (${formatBytes(sizeBytes)}) - ${reason}`);
574
+ if (dryRun) {
575
+ p.log.info("Dry run - no changes made.");
576
+ return {
577
+ removed: previewResult.removed.map((r) => ({
578
+ repo: r.repo,
579
+ reason: r.reason,
580
+ sizeMB: Math.round(r.sizeBytes / (1024 * 1024))
581
+ })),
582
+ freedMB: Math.round(previewResult.freedBytes / (1024 * 1024))
583
+ };
584
+ }
585
+ let shouldProceed = yes;
586
+ if (!yes) {
587
+ const confirm = await p.confirm({ message: "Proceed with removal?" });
588
+ if (p.isCancel(confirm) || !confirm) {
589
+ p.log.info("Cancelled.");
590
+ return {
591
+ removed: [],
592
+ freedMB: 0
593
+ };
594
+ }
595
+ shouldProceed = true;
596
+ }
597
+ if (shouldProceed) {
598
+ const result = await gcRepos({
599
+ olderThanDays,
600
+ withoutReference,
601
+ dryRun: false
602
+ });
603
+ p.log.success(`Removed ${result.removed.length} repos, freed ${formatBytes(result.freedBytes)}`);
604
+ return {
605
+ removed: result.removed.map((r) => ({
606
+ repo: r.repo,
607
+ reason: r.reason,
608
+ sizeMB: Math.round(r.sizeBytes / (1024 * 1024))
609
+ })),
610
+ freedMB: Math.round(result.freedBytes / (1024 * 1024))
611
+ };
612
+ }
613
+ return {
614
+ removed: [],
615
+ freedMB: 0
616
+ };
617
+ }
618
+ async function repoDiscoverHandler(options) {
619
+ const { dryRun = false, yes = false } = options;
620
+ const repoRoot = getRepoRoot(loadConfig());
621
+ if (!existsSync(repoRoot)) {
622
+ p.log.error(`Repo root does not exist: ${repoRoot}`);
623
+ return {
624
+ discovered: 0,
625
+ alreadyIndexed: 0
626
+ };
627
+ }
628
+ const previewResult = await discoverRepos({
629
+ repoRoot,
630
+ dryRun: true
631
+ });
632
+ if (previewResult.discovered.length === 0) {
633
+ if (previewResult.alreadyIndexed > 0) p.log.info(`All ${previewResult.alreadyIndexed} repos already indexed.`);
634
+ else p.log.info("No repos found to discover.");
635
+ return {
636
+ discovered: 0,
637
+ alreadyIndexed: previewResult.alreadyIndexed
638
+ };
639
+ }
640
+ p.log.info(`Found ${previewResult.discovered.length} unindexed repos:`);
641
+ for (const repo of previewResult.discovered.slice(0, 20)) console.log(` + ${repo.fullName}`);
642
+ if (previewResult.discovered.length > 20) console.log(` ... and ${previewResult.discovered.length - 20} more`);
643
+ if (dryRun) {
644
+ p.log.info("Dry run - no changes made.");
645
+ return {
646
+ discovered: previewResult.discovered.length,
647
+ alreadyIndexed: previewResult.alreadyIndexed
648
+ };
649
+ }
650
+ let shouldProceed = yes;
651
+ if (!yes) {
652
+ const confirm = await p.confirm({ message: "Add these repos to the index?" });
653
+ if (p.isCancel(confirm) || !confirm) {
654
+ p.log.info("Cancelled.");
655
+ return {
656
+ discovered: 0,
657
+ alreadyIndexed: previewResult.alreadyIndexed
658
+ };
659
+ }
660
+ shouldProceed = true;
661
+ }
662
+ if (shouldProceed) {
663
+ const result = await discoverRepos({ repoRoot });
664
+ p.log.success(`Added ${result.discovered.length} repos to clone map (marked as not referenced)`);
665
+ return {
666
+ discovered: result.discovered.length,
667
+ alreadyIndexed: result.alreadyIndexed
668
+ };
669
+ }
670
+ return {
671
+ discovered: 0,
672
+ alreadyIndexed: previewResult.alreadyIndexed
673
+ };
674
+ }
675
+
676
+ //#endregion
677
+ //#region src/handlers/push.ts
678
+ /**
679
+ * Push command handler
680
+ */
681
+ const DESCRIPTION_MAX = 200;
682
+ function extractDescription(referenceContent, fallback) {
683
+ const lines = referenceContent.split(/\r?\n/);
684
+ let sawTitle = false;
685
+ let description = "";
686
+ for (let i = 0; i < lines.length; i++) {
687
+ const line = lines[i]?.trim() ?? "";
688
+ if (!line) continue;
689
+ if (line.startsWith("# ")) {
690
+ sawTitle = true;
691
+ continue;
692
+ }
693
+ if (!sawTitle || line.startsWith("#")) continue;
694
+ description = line;
695
+ for (let j = i + 1; j < lines.length; j++) {
696
+ const next = lines[j]?.trim() ?? "";
697
+ if (!next || next.startsWith("#")) break;
698
+ description += ` ${next}`;
699
+ }
700
+ break;
701
+ }
702
+ description = description.replace(/\s+/g, " ").trim();
703
+ if (!description) description = fallback;
704
+ if (description.length > DESCRIPTION_MAX) description = description.slice(0, DESCRIPTION_MAX).trim();
705
+ if (!description) description = fallback.slice(0, DESCRIPTION_MAX);
706
+ return description;
707
+ }
708
+ /**
709
+ * Load local reference data from disk.
710
+ * Format: reference file + meta.json
711
+ */
712
+ function loadLocalReference(metaDir, referencePath, fullName) {
713
+ const metaPath = join(metaDir, "meta.json");
714
+ if (!existsSync(referencePath) || !existsSync(metaPath)) return null;
715
+ try {
716
+ const referenceContent = readFileSync(referencePath, "utf-8");
717
+ const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
718
+ return {
719
+ fullName: "",
720
+ referenceName: toReferenceName(fullName),
721
+ referenceDescription: extractDescription(referenceContent, `Reference for ${fullName}`),
722
+ referenceContent,
723
+ commitSha: meta.commitSha,
724
+ generatedAt: meta.referenceUpdatedAt
725
+ };
726
+ } catch {
727
+ return null;
728
+ }
729
+ }
730
+ /**
731
+ * Main push handler
732
+ * Uploads local reference to offworld.sh
733
+ */
734
+ async function pushHandler(options) {
735
+ const { repo } = options;
736
+ const s = createSpinner();
737
+ try {
738
+ let token;
739
+ try {
740
+ token = await getToken();
741
+ } catch (err) {
742
+ if (err instanceof NotLoggedInError || err instanceof TokenExpiredError) {
743
+ p.log.error(err.message);
744
+ return {
745
+ success: false,
746
+ message: err.message
747
+ };
748
+ }
749
+ throw err;
750
+ }
751
+ s.start("Parsing repository...");
752
+ const source = parseRepoInput(repo);
753
+ s.stop("Repository parsed");
754
+ if (source.type === "local") {
755
+ p.log.error("Local repositories cannot be pushed to offworld.sh.");
756
+ p.log.info("Only remote GitHub repositories can be pushed.");
757
+ return {
758
+ success: false,
759
+ message: "Local repositories not supported"
760
+ };
761
+ }
762
+ if (source.provider !== "github") {
763
+ p.log.error(`${source.provider} repositories are not yet supported.`);
764
+ p.log.info("GitHub support only for now - GitLab and Bitbucket coming soon!");
765
+ return {
766
+ success: false,
767
+ message: "Only GitHub repositories supported"
768
+ };
769
+ }
770
+ if (!isRepoCloned(source.qualifiedName)) {
771
+ p.log.error(`Repository ${source.fullName} is not cloned locally.`);
772
+ p.log.info(`Run 'ow pull ${source.fullName}' first to clone and analyze.`);
773
+ return {
774
+ success: false,
775
+ message: "Repository not cloned locally"
776
+ };
777
+ }
778
+ s.start("Loading local reference...");
779
+ const metaDir = getMetaPath(source.fullName);
780
+ const referencePath = getReferencePath(source.fullName);
781
+ if (!existsSync(metaDir) || !existsSync(referencePath)) {
782
+ s.stop("No reference found");
783
+ p.log.error(`No reference found for ${source.fullName}.`);
784
+ p.log.info(`Run 'ow generate ${source.fullName}' to generate reference.`);
785
+ return {
786
+ success: false,
787
+ message: "No local reference found"
788
+ };
789
+ }
790
+ const localReference = loadLocalReference(metaDir, referencePath, source.fullName);
791
+ if (!localReference) {
792
+ s.stop("Invalid reference");
793
+ p.log.error("Local reference is incomplete or corrupted.");
794
+ p.log.info(`Run 'ow generate ${source.fullName} --force' to regenerate.`);
795
+ return {
796
+ success: false,
797
+ message: "Local reference incomplete"
798
+ };
799
+ }
800
+ localReference.fullName = source.fullName;
801
+ const repoPath = getClonedRepoPath(source.qualifiedName);
802
+ if (repoPath) {
803
+ const currentSha = getCommitSha(repoPath);
804
+ if (currentSha !== localReference.commitSha) {
805
+ s.stop("Reference outdated");
806
+ p.log.warn("Local reference was generated for a different commit.");
807
+ p.log.info(`Reference: ${localReference.commitSha.slice(0, 7)}`);
808
+ p.log.info(`Current: ${currentSha.slice(0, 7)}`);
809
+ p.log.info(`Run 'ow generate ${source.fullName} --force' to regenerate.`);
810
+ return {
811
+ success: false,
812
+ message: "Reference outdated - run generate to update"
813
+ };
814
+ }
815
+ }
816
+ s.stop("Reference loaded");
817
+ s.start("Uploading to offworld.sh...");
818
+ try {
819
+ const result = await pushReference(localReference, token);
820
+ if (result.success) {
821
+ s.stop("Reference uploaded!");
822
+ p.log.success(`Successfully pushed reference for ${source.fullName}`);
823
+ p.log.info(`View at: https://offworld.sh/${source.owner}/${source.repo}`);
824
+ return {
825
+ success: true,
826
+ message: "Reference pushed successfully"
827
+ };
828
+ }
829
+ s.stop("Upload failed");
830
+ p.log.error(result.message || "Unknown error during upload");
831
+ return {
832
+ success: false,
833
+ message: result.message || "Upload failed"
834
+ };
835
+ } catch (err) {
836
+ s.stop("Upload failed");
837
+ if (err instanceof AuthenticationError) {
838
+ p.log.error("Authentication failed.");
839
+ p.log.info("Please run 'ow auth login' again.");
840
+ return {
841
+ success: false,
842
+ message: "Authentication failed"
843
+ };
844
+ }
845
+ if (err instanceof RateLimitError) {
846
+ p.log.error("Rate limit exceeded.");
847
+ p.log.info("You can push up to 20 references per day.");
848
+ p.log.info("Please try again tomorrow.");
849
+ return {
850
+ success: false,
851
+ message: "Rate limit exceeded"
852
+ };
853
+ }
854
+ if (err instanceof CommitExistsError) {
855
+ p.log.error("A reference already exists for this commit.");
856
+ p.log.info(`Commit: ${localReference.commitSha.slice(0, 7)}`);
857
+ p.log.info("References are immutable per commit. Update the repo and regenerate to push a new version.");
858
+ return {
859
+ success: false,
860
+ message: "Reference already exists for this commit"
861
+ };
862
+ }
863
+ if (err instanceof InvalidInputError) {
864
+ p.log.error("Invalid input data.");
865
+ p.log.info(err.message);
866
+ return {
867
+ success: false,
868
+ message: err.message
869
+ };
870
+ }
871
+ if (err instanceof InvalidReferenceError) {
872
+ p.log.error("Invalid reference content.");
873
+ p.log.info(err.message);
874
+ p.log.info(`Run 'ow generate ${source.fullName} --force' to regenerate.`);
875
+ return {
876
+ success: false,
877
+ message: err.message
878
+ };
879
+ }
880
+ if (err instanceof SyncRepoNotFoundError) {
881
+ p.log.error("Repository not found on GitHub.");
882
+ p.log.info("Ensure the repository exists and is public.");
883
+ return {
884
+ success: false,
885
+ message: "Repository not found"
886
+ };
887
+ }
888
+ if (err instanceof LowStarsError) {
889
+ p.log.error("Repository does not meet star requirements.");
890
+ p.log.info("Repositories need at least 5 stars to be pushed to offworld.sh.");
891
+ p.log.info("This helps ensure quality skills for the community.");
892
+ return {
893
+ success: false,
894
+ message: "Repository needs 5+ stars"
895
+ };
896
+ }
897
+ if (err instanceof PrivateRepoError) {
898
+ p.log.error("Private repositories are not supported.");
899
+ p.log.info("Only public GitHub repositories can be pushed to offworld.sh.");
900
+ return {
901
+ success: false,
902
+ message: "Private repos not supported"
903
+ };
904
+ }
905
+ if (err instanceof CommitNotFoundError) {
906
+ p.log.error("Commit not found in repository.");
907
+ p.log.info("The analyzed commit may have been rebased or removed.");
908
+ p.log.info(`Run 'ow generate ${source.fullName} --force' to regenerate with current commit.`);
909
+ return {
910
+ success: false,
911
+ message: "Commit not found"
912
+ };
913
+ }
914
+ if (err instanceof GitHubError) {
915
+ p.log.error("GitHub API error.");
916
+ p.log.info(err.message);
917
+ p.log.info("Please try again later.");
918
+ return {
919
+ success: false,
920
+ message: "GitHub API error"
921
+ };
922
+ }
923
+ throw err;
924
+ }
925
+ } catch (error) {
926
+ s.stop("Failed");
927
+ const message = error instanceof Error ? error.message : "Unknown error";
928
+ p.log.error(message);
929
+ return {
930
+ success: false,
931
+ message
932
+ };
933
+ }
934
+ }
935
+
936
+ //#endregion
937
+ //#region src/handlers/remove.ts
938
+ /**
939
+ * Remove command handler
940
+ */
941
+ function getAffectedPathsFromMap(qualifiedName) {
942
+ const entry = readGlobalMap().repos[qualifiedName];
943
+ if (!entry) return null;
944
+ const repoPath = entry.localPath;
945
+ const referenceFileName = entry.primary || "";
946
+ const referencePath = referenceFileName ? join(Paths.offworldReferencesDir, referenceFileName) : void 0;
947
+ return {
948
+ repoPath: existsSync(repoPath) ? repoPath : void 0,
949
+ referencePath: referencePath && existsSync(referencePath) ? referencePath : void 0,
950
+ symlinkPaths: []
951
+ };
952
+ }
953
+ async function rmHandler(options) {
954
+ const { repo, yes = false, referenceOnly = false, repoOnly = false, dryRun = false } = options;
955
+ try {
956
+ const source = parseRepoInput(repo);
957
+ const qualifiedName = source.qualifiedName;
958
+ const repoName = source.type === "remote" ? source.fullName : source.name;
959
+ if (referenceOnly && repoOnly) {
960
+ p.log.error("Cannot use --reference-only and --repo-only together");
961
+ return {
962
+ success: false,
963
+ message: "Invalid options"
964
+ };
965
+ }
966
+ const entry = readGlobalMap().repos[qualifiedName];
967
+ if (!entry && !referenceOnly) {
968
+ p.log.warn(`Repository not found in map: ${repo}`);
969
+ return {
970
+ success: false,
971
+ message: "Repository not found"
972
+ };
973
+ }
974
+ if (referenceOnly && !entry) return handleReferenceOnlyRemoval(repoName, yes, dryRun);
975
+ const affected = getAffectedPathsFromMap(qualifiedName);
976
+ if (dryRun || !yes) {
977
+ p.log.info("The following will be removed:");
978
+ if (!referenceOnly && affected.repoPath) console.log(` Repository: ${affected.repoPath}`);
979
+ if (!repoOnly && affected.referencePath) console.log(` Reference: ${affected.referencePath}`);
980
+ console.log("");
981
+ }
982
+ if (dryRun) {
983
+ p.log.info("Dry run - no files were deleted.");
984
+ return {
985
+ success: true,
986
+ removed: affected
987
+ };
988
+ }
989
+ if (!yes) {
990
+ const what = referenceOnly ? "reference files" : repoOnly ? "repository" : qualifiedName;
991
+ const confirm = await p.confirm({ message: `Are you sure you want to remove ${what}?` });
992
+ if (p.isCancel(confirm) || !confirm) {
993
+ p.log.info("Aborted.");
994
+ return {
995
+ success: false,
996
+ message: "Aborted by user"
997
+ };
998
+ }
999
+ }
1000
+ const s = createSpinner();
1001
+ const action = referenceOnly ? "Removing reference files..." : repoOnly ? "Removing repository..." : "Removing...";
1002
+ s.start(action);
1003
+ if (await removeRepo(qualifiedName, {
1004
+ referenceOnly,
1005
+ repoOnly
1006
+ })) {
1007
+ const doneMsg = referenceOnly ? "Reference files removed" : repoOnly ? "Repository removed" : "Removed";
1008
+ s.stop(doneMsg);
1009
+ p.log.success(`Removed: ${qualifiedName}`);
1010
+ return {
1011
+ success: true,
1012
+ removed: affected
1013
+ };
1014
+ } else {
1015
+ s.stop("Failed to remove");
1016
+ return {
1017
+ success: false,
1018
+ message: "Failed to remove repository"
1019
+ };
1020
+ }
1021
+ } catch (error) {
1022
+ const message = error instanceof Error ? error.message : "Unknown error";
1023
+ p.log.error(message);
1024
+ return {
1025
+ success: false,
1026
+ message
1027
+ };
1028
+ }
1029
+ }
1030
+ async function handleReferenceOnlyRemoval(repoName, yes, dryRun) {
1031
+ const referenceFileName = toReferenceFileName(repoName);
1032
+ const referencePath = join(Paths.offworldReferencesDir, referenceFileName);
1033
+ const metaPath = getMetaPath(repoName);
1034
+ if (!existsSync(referencePath) && !existsSync(metaPath)) {
1035
+ p.log.warn(`No reference files found for: ${repoName}`);
1036
+ return {
1037
+ success: false,
1038
+ message: "No reference files found"
1039
+ };
1040
+ }
1041
+ if (dryRun || !yes) {
1042
+ p.log.info("The following will be removed:");
1043
+ if (existsSync(referencePath)) console.log(` Reference: ${referencePath}`);
1044
+ if (existsSync(metaPath)) console.log(` Meta: ${metaPath}`);
1045
+ console.log("");
1046
+ }
1047
+ if (dryRun) {
1048
+ p.log.info("Dry run - no files were deleted.");
1049
+ return {
1050
+ success: true,
1051
+ removed: { referencePath }
1052
+ };
1053
+ }
1054
+ if (!yes) {
1055
+ const confirm = await p.confirm({ message: `Are you sure you want to remove reference files for ${repoName}?` });
1056
+ if (p.isCancel(confirm) || !confirm) {
1057
+ p.log.info("Aborted.");
1058
+ return {
1059
+ success: false,
1060
+ message: "Aborted by user"
1061
+ };
1062
+ }
1063
+ }
1064
+ const s = createSpinner();
1065
+ s.start("Removing reference files...");
1066
+ if (existsSync(referencePath)) rmSync(referencePath, { force: true });
1067
+ if (existsSync(metaPath)) rmSync(metaPath, {
1068
+ recursive: true,
1069
+ force: true
1070
+ });
1071
+ s.stop("Reference files removed");
1072
+ p.log.success(`Removed reference files for: ${repoName}`);
1073
+ return {
1074
+ success: true,
1075
+ removed: { referencePath }
1076
+ };
1077
+ }
1078
+
1079
+ //#endregion
1080
+ //#region src/handlers/config.ts
1081
+ /**
1082
+ * Config command handlers
1083
+ */
1084
+ const VALID_KEYS = [
1085
+ "repoRoot",
1086
+ "defaultShallow",
1087
+ "defaultModel",
1088
+ "agents"
1089
+ ];
1090
+ function isValidKey(key) {
1091
+ return VALID_KEYS.includes(key);
1092
+ }
1093
+ async function configShowHandler(options) {
1094
+ const config = loadConfig();
1095
+ const projectMapPath = resolve(process.cwd(), ".offworld/map.json");
1096
+ const hasProjectMap = existsSync(projectMapPath);
1097
+ const paths = {
1098
+ skillDir: join(Paths.data, "skill", "offworld"),
1099
+ referencesDir: join(Paths.data, "skill", "offworld", "references"),
1100
+ globalMap: join(Paths.data, "skill", "offworld", "assets", "map.json")
1101
+ };
1102
+ if (hasProjectMap) paths.projectMap = projectMapPath;
1103
+ if (options.json) {
1104
+ const output = {
1105
+ ...config,
1106
+ paths
1107
+ };
1108
+ console.log(JSON.stringify(output, null, 2));
1109
+ } else {
1110
+ p.log.info("Current configuration:\n");
1111
+ for (const [key, value] of Object.entries(config)) console.log(` ${key}: ${JSON.stringify(value)}`);
1112
+ console.log("");
1113
+ p.log.info(`Config file: ${getConfigPath()}`);
1114
+ if (hasProjectMap) p.log.info(`Project map: ${projectMapPath}`);
1115
+ }
1116
+ return {
1117
+ config,
1118
+ paths
1119
+ };
1120
+ }
1121
+ async function configSetHandler(options) {
1122
+ const { key, value } = options;
1123
+ if (!isValidKey(key)) {
1124
+ p.log.error(`Invalid config key: ${key}`);
1125
+ p.log.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
1126
+ return {
1127
+ success: false,
1128
+ message: `Invalid key: ${key}`
1129
+ };
1130
+ }
1131
+ let parsedValue;
1132
+ if (key === "defaultShallow") if (value === "true" || value === "1") parsedValue = true;
1133
+ else if (value === "false" || value === "0") parsedValue = false;
1134
+ else {
1135
+ p.log.error(`Invalid boolean value: ${value}. Use 'true' or 'false'.`);
1136
+ return {
1137
+ success: false,
1138
+ message: `Invalid boolean value: ${value}`
1139
+ };
1140
+ }
1141
+ else if (key === "agents") {
1142
+ const agentValues = value.split(",").map((a) => a.trim()).filter(Boolean);
1143
+ const validAgents = AgentSchema.options;
1144
+ const invalidAgents = agentValues.filter((a) => !validAgents.includes(a));
1145
+ if (invalidAgents.length > 0) {
1146
+ p.log.error(`Invalid agent(s): ${invalidAgents.join(", ")}`);
1147
+ p.log.info(`Valid agents: ${validAgents.join(", ")}`);
1148
+ return {
1149
+ success: false,
1150
+ message: `Invalid agents: ${invalidAgents.join(", ")}`
1151
+ };
1152
+ }
1153
+ parsedValue = agentValues;
1154
+ } else parsedValue = value;
1155
+ try {
1156
+ saveConfig({ [key]: parsedValue });
1157
+ p.log.success(`Set ${key} = ${JSON.stringify(parsedValue)}`);
1158
+ return {
1159
+ success: true,
1160
+ key,
1161
+ value: parsedValue
1162
+ };
1163
+ } catch (error) {
1164
+ const message = error instanceof Error ? error.message : "Unknown error";
1165
+ p.log.error(message);
1166
+ return {
1167
+ success: false,
1168
+ message
1169
+ };
1170
+ }
1171
+ }
1172
+ async function configGetHandler(options) {
1173
+ const { key } = options;
1174
+ const config = loadConfig();
1175
+ if (!isValidKey(key)) {
1176
+ p.log.error(`Invalid config key: ${key}`);
1177
+ p.log.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
1178
+ return {
1179
+ key,
1180
+ value: null
1181
+ };
1182
+ }
1183
+ const value = config[key];
1184
+ console.log(JSON.stringify(value));
1185
+ return {
1186
+ key,
1187
+ value
1188
+ };
1189
+ }
1190
+ async function configResetHandler() {
1191
+ try {
1192
+ const defaults = ConfigSchema.parse({});
1193
+ saveConfig(defaults);
1194
+ p.log.success("Configuration reset to defaults:");
1195
+ for (const [key, value] of Object.entries(defaults)) console.log(` ${key}: ${JSON.stringify(value)}`);
1196
+ return { success: true };
1197
+ } catch (error) {
1198
+ const message = error instanceof Error ? error.message : "Unknown error";
1199
+ p.log.error(message);
1200
+ return { success: false };
1201
+ }
1202
+ }
1203
+ async function configPathHandler() {
1204
+ const path = getConfigPath();
1205
+ console.log(path);
1206
+ return { path };
1207
+ }
1208
+ async function configAgentsHandler() {
1209
+ const config = loadConfig();
1210
+ const detectedAgents = detectInstalledAgents();
1211
+ const allAgentConfigs = getAllAgentConfigs();
1212
+ if (detectedAgents.length > 0) {
1213
+ const detectedNames = detectedAgents.map((a) => allAgentConfigs.find((c) => c.name === a)?.displayName ?? a).join(", ");
1214
+ p.log.info(`Detected agents: ${detectedNames}`);
1215
+ }
1216
+ const agentOptions = allAgentConfigs.map((cfg) => ({
1217
+ value: cfg.name,
1218
+ label: cfg.displayName,
1219
+ hint: cfg.globalSkillsDir
1220
+ }));
1221
+ const initialAgents = config.agents && config.agents.length > 0 ? config.agents : detectedAgents;
1222
+ const agentsResult = await p.multiselect({
1223
+ message: "Select agents to install skills to",
1224
+ options: agentOptions,
1225
+ initialValues: initialAgents,
1226
+ required: false
1227
+ });
1228
+ if (p.isCancel(agentsResult)) {
1229
+ p.log.warn("Cancelled");
1230
+ return { success: false };
1231
+ }
1232
+ const agents = agentsResult;
1233
+ try {
1234
+ saveConfig({ agents });
1235
+ p.log.success(`Agents set to: ${agents.join(", ") || "(none)"}`);
1236
+ return {
1237
+ success: true,
1238
+ agents
1239
+ };
1240
+ } catch (error) {
1241
+ const message = error instanceof Error ? error.message : "Unknown error";
1242
+ p.log.error(message);
1243
+ return { success: false };
1244
+ }
1245
+ }
1246
+
1247
+ //#endregion
1248
+ //#region src/handlers/auth.ts
1249
+ /**
1250
+ * Auth command handlers
1251
+ * Uses WorkOS Device Authorization Grant for CLI authentication
1252
+ */
1253
+ const WORKOS_API = "https://api.workos.com";
1254
+ function getWorkosClientId() {
1255
+ return process.env.WORKOS_CLIENT_ID || "";
1256
+ }
1257
+ function extractJwtExpiration(token) {
1258
+ try {
1259
+ const parts = token.split(".");
1260
+ if (parts.length !== 3) return void 0;
1261
+ const payload = parts[1];
1262
+ if (!payload) return void 0;
1263
+ const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
1264
+ if (typeof decoded.exp !== "number") return void 0;
1265
+ return (/* @__PURE__ */ new Date(decoded.exp * 1e3)).toISOString();
1266
+ } catch {
1267
+ return;
1268
+ }
1269
+ }
1270
+ async function requestDeviceCode() {
1271
+ const response = await fetch(`${WORKOS_API}/user_management/authorize/device`, {
1272
+ method: "POST",
1273
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1274
+ body: new URLSearchParams({ client_id: getWorkosClientId() })
1275
+ });
1276
+ if (!response.ok) {
1277
+ const error = await response.text();
1278
+ throw new Error(`Failed to request device code: ${error}`);
1279
+ }
1280
+ return response.json();
1281
+ }
1282
+ async function pollForTokens(deviceCode, interval, onStatus) {
1283
+ let pollInterval = interval;
1284
+ while (true) {
1285
+ const response = await fetch(`${WORKOS_API}/user_management/authenticate`, {
1286
+ method: "POST",
1287
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1288
+ body: new URLSearchParams({
1289
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
1290
+ device_code: deviceCode,
1291
+ client_id: getWorkosClientId()
1292
+ })
1293
+ });
1294
+ if (response.ok) return response.json();
1295
+ const data = await response.json();
1296
+ switch (data.error) {
1297
+ case "authorization_pending":
1298
+ onStatus?.("Waiting for approval...");
1299
+ await sleep(pollInterval * 1e3);
1300
+ break;
1301
+ case "slow_down":
1302
+ pollInterval += 5;
1303
+ onStatus?.("Slowing down polling...");
1304
+ await sleep(pollInterval * 1e3);
1305
+ break;
1306
+ case "access_denied": throw new Error("Authorization denied by user");
1307
+ case "expired_token": throw new Error("Device code expired. Please try again.");
1308
+ default: throw new Error(`Authentication failed: ${data.error}`);
1309
+ }
1310
+ }
1311
+ }
1312
+ function sleep(ms) {
1313
+ return new Promise((resolve) => setTimeout(resolve, ms));
1314
+ }
1315
+ async function authLoginHandler() {
1316
+ if (!getWorkosClientId()) {
1317
+ p.log.error("WORKOS_CLIENT_ID not configured.");
1318
+ p.log.info("For local development, create apps/cli/.env with:");
1319
+ p.log.info(" WORKOS_CLIENT_ID=your_client_id");
1320
+ p.log.info(" CONVEX_URL=your_convex_url");
1321
+ return {
1322
+ success: false,
1323
+ message: "WORKOS_CLIENT_ID not configured"
1324
+ };
1325
+ }
1326
+ const s = createSpinner();
1327
+ const currentStatus = await getAuthStatus();
1328
+ if (currentStatus.isLoggedIn) {
1329
+ p.log.info(`Already logged in as ${currentStatus.email || "unknown user"}`);
1330
+ const shouldRelogin = await p.confirm({ message: "Do you want to log in again?" });
1331
+ if (!shouldRelogin || p.isCancel(shouldRelogin)) return {
1332
+ success: true,
1333
+ email: currentStatus.email
1334
+ };
1335
+ }
1336
+ s.start("Requesting device code...");
1337
+ let deviceData;
1338
+ try {
1339
+ deviceData = await requestDeviceCode();
1340
+ } catch (error) {
1341
+ s.stop("Failed to get device code");
1342
+ return {
1343
+ success: false,
1344
+ message: error instanceof Error ? error.message : "Unknown error"
1345
+ };
1346
+ }
1347
+ s.stop();
1348
+ p.log.info(`\nOpen this URL in your browser:\n`);
1349
+ p.log.info(` ${deviceData.verification_uri_complete}\n`);
1350
+ p.log.info(`Or go to ${deviceData.verification_uri} and enter code: ${deviceData.user_code}\n`);
1351
+ try {
1352
+ await open(deviceData.verification_uri_complete);
1353
+ } catch {}
1354
+ s.start("Waiting for approval...");
1355
+ try {
1356
+ const tokenData = await pollForTokens(deviceData.device_code, deviceData.interval, (status) => {
1357
+ s.message(status);
1358
+ });
1359
+ s.stop("Login successful!");
1360
+ const expiresAt = tokenData.expires_at ? (/* @__PURE__ */ new Date(tokenData.expires_at * 1e3)).toISOString() : extractJwtExpiration(tokenData.access_token);
1361
+ saveAuthData({
1362
+ token: tokenData.access_token,
1363
+ email: tokenData.user.email,
1364
+ workosId: tokenData.user.id,
1365
+ refreshToken: tokenData.refresh_token,
1366
+ expiresAt
1367
+ });
1368
+ p.log.success(`Logged in as ${tokenData.user.email}`);
1369
+ return {
1370
+ success: true,
1371
+ email: tokenData.user.email
1372
+ };
1373
+ } catch (error) {
1374
+ s.stop("Login failed");
1375
+ return {
1376
+ success: false,
1377
+ message: error instanceof Error ? error.message : "Unknown error"
1378
+ };
1379
+ }
1380
+ }
1381
+ /**
1382
+ * Handles 'ow auth logout' command
1383
+ * Clears stored session token
1384
+ */
1385
+ async function authLogoutHandler() {
1386
+ if (!(await getAuthStatus()).isLoggedIn) {
1387
+ p.log.info("Not currently logged in");
1388
+ return {
1389
+ success: true,
1390
+ message: "Not logged in"
1391
+ };
1392
+ }
1393
+ if (clearAuthData()) {
1394
+ p.log.success("Logged out successfully");
1395
+ return {
1396
+ success: true,
1397
+ message: "Logged out successfully"
1398
+ };
1399
+ }
1400
+ p.log.warn("Could not clear auth data");
1401
+ return {
1402
+ success: false,
1403
+ message: "Could not clear auth data"
1404
+ };
1405
+ }
1406
+ /**
1407
+ * Handles 'ow auth status' command
1408
+ * Shows current authentication status
1409
+ */
1410
+ async function authStatusHandler() {
1411
+ const status = await getAuthStatus();
1412
+ if (status.isLoggedIn) {
1413
+ p.log.info(`Logged in as: ${status.email || "unknown"}`);
1414
+ if (status.expiresAt) {
1415
+ const expiresDate = new Date(status.expiresAt);
1416
+ p.log.info(`Session expires: ${expiresDate.toLocaleString()}`);
1417
+ }
1418
+ p.log.info(`Auth file: ${getAuthPath()}`);
1419
+ } else {
1420
+ p.log.info("Not logged in");
1421
+ p.log.info("Run 'ow auth login' to authenticate");
1422
+ }
1423
+ return {
1424
+ loggedIn: status.isLoggedIn,
1425
+ email: status.email,
1426
+ workosId: status.workosId,
1427
+ expiresAt: status.expiresAt
1428
+ };
1429
+ }
1430
+
1431
+ //#endregion
1432
+ //#region src/handlers/init.ts
1433
+ const DEFAULT_PROVIDER = "anthropic";
1434
+ const MAX_SELECT_ITEMS = 15;
1435
+ function expandTilde(path) {
1436
+ if (path.startsWith("~/")) return resolve(homedir(), path.slice(2));
1437
+ return resolve(path);
1438
+ }
1439
+ function collapseTilde(path) {
1440
+ const home = homedir();
1441
+ if (path.startsWith(home)) return "~" + path.slice(home.length);
1442
+ return path;
1443
+ }
1444
+ function validatePath(value) {
1445
+ if (!value.trim()) return "Path cannot be empty";
1446
+ }
1447
+ function detectProjectRoot$1() {
1448
+ let currentDir = process.cwd();
1449
+ while (currentDir !== homedir()) {
1450
+ if (existsSync(join(currentDir, "package.json"))) return currentDir;
1451
+ const parent = dirname(currentDir);
1452
+ if (parent === currentDir) break;
1453
+ currentDir = parent;
1454
+ }
1455
+ return null;
1456
+ }
1457
+ async function initHandler(options = {}) {
1458
+ const configPath = getConfigPath();
1459
+ const existingConfig = loadConfig();
1460
+ const configExists = existsSync(configPath);
1461
+ const projectRoot = detectProjectRoot$1();
1462
+ p.intro("ow init");
1463
+ if (!options.skipAuth) {
1464
+ if (!(await getAuthStatus()).isLoggedIn) {
1465
+ p.log.info("You are not logged in");
1466
+ const shouldLogin = await p.confirm({
1467
+ message: "Do you want to log in now?",
1468
+ initialValue: true
1469
+ });
1470
+ if (!p.isCancel(shouldLogin) && shouldLogin) await authLoginHandler();
1471
+ }
1472
+ }
1473
+ if (configExists) {
1474
+ if (!options.force) {
1475
+ if (projectRoot) {
1476
+ p.log.warn(`Global config already exists at ${configPath}`);
1477
+ p.log.info("");
1478
+ p.log.info("Did you mean to run project setup? Use:");
1479
+ p.log.info(" ow project init");
1480
+ p.log.info("");
1481
+ p.log.info("To reconfigure global settings, use:");
1482
+ p.log.info(" ow init --force");
1483
+ p.outro("");
1484
+ return {
1485
+ success: false,
1486
+ configPath
1487
+ };
1488
+ }
1489
+ p.log.warn("Already configured. Use --force to reconfigure.");
1490
+ p.outro("");
1491
+ return {
1492
+ success: false,
1493
+ configPath
1494
+ };
1495
+ }
1496
+ p.log.info("Reconfiguring global settings...");
1497
+ }
1498
+ let repoRoot;
1499
+ if (options.repoRoot) repoRoot = collapseTilde(expandTilde(options.repoRoot));
1500
+ else {
1501
+ const repoRootResult = await p.text({
1502
+ message: "Where should repositories be cloned?",
1503
+ placeholder: "~/ow",
1504
+ initialValue: existingConfig.repoRoot,
1505
+ validate: validatePath
1506
+ });
1507
+ if (p.isCancel(repoRootResult)) {
1508
+ p.outro("Setup cancelled");
1509
+ return {
1510
+ success: false,
1511
+ configPath
1512
+ };
1513
+ }
1514
+ repoRoot = collapseTilde(expandTilde(repoRootResult));
1515
+ }
1516
+ let provider;
1517
+ let model;
1518
+ if (options.model) {
1519
+ const parts = options.model.split("/");
1520
+ if (parts.length !== 2) {
1521
+ p.log.error(`Invalid model format. Expected 'provider/model', got '${options.model}'`);
1522
+ p.outro("Setup failed");
1523
+ return {
1524
+ success: false,
1525
+ configPath
1526
+ };
1527
+ }
1528
+ provider = parts[0];
1529
+ model = parts[1];
1530
+ const validation = await validateProviderModel(provider, model);
1531
+ if (!validation.valid) {
1532
+ p.log.error(validation.error);
1533
+ p.outro("Setup failed");
1534
+ return {
1535
+ success: false,
1536
+ configPath
1537
+ };
1538
+ }
1539
+ } else {
1540
+ const spin = p.spinner();
1541
+ spin.start("Fetching available providers...");
1542
+ let providers;
1543
+ try {
1544
+ providers = await listProviders();
1545
+ } catch (err) {
1546
+ spin.stop("Failed to fetch providers");
1547
+ p.log.error(err instanceof Error ? err.message : "Network error");
1548
+ p.outro("Setup failed");
1549
+ return {
1550
+ success: false,
1551
+ configPath
1552
+ };
1553
+ }
1554
+ spin.stop("Loaded providers from models.dev");
1555
+ const priorityProviders = [
1556
+ "opencode",
1557
+ "anthropic",
1558
+ "openai",
1559
+ "google"
1560
+ ];
1561
+ const providerOptions = [...providers.filter((p) => priorityProviders.includes(p.id)), ...providers.filter((p) => !priorityProviders.includes(p.id)).sort((a, b) => a.name.localeCompare(b.name))].map((prov) => ({
1562
+ value: prov.id,
1563
+ label: prov.name
1564
+ }));
1565
+ const currentProvider = existingConfig.defaultModel?.split("/")[0];
1566
+ const providerResult = await p.select({
1567
+ message: "Select your AI provider",
1568
+ options: providerOptions,
1569
+ initialValue: currentProvider ?? DEFAULT_PROVIDER,
1570
+ maxItems: MAX_SELECT_ITEMS
1571
+ });
1572
+ if (p.isCancel(providerResult)) {
1573
+ p.outro("Setup cancelled");
1574
+ return {
1575
+ success: false,
1576
+ configPath
1577
+ };
1578
+ }
1579
+ provider = providerResult;
1580
+ spin.start("Fetching models...");
1581
+ const providerData = await getProvider(provider);
1582
+ spin.stop(`Loaded ${providerData?.models.length ?? 0} models`);
1583
+ if (!providerData || providerData.models.length === 0) {
1584
+ p.log.error(`No models found for provider "${provider}"`);
1585
+ p.outro("Setup failed");
1586
+ return {
1587
+ success: false,
1588
+ configPath
1589
+ };
1590
+ }
1591
+ const modelOptions = providerData.models.map((m) => ({
1592
+ value: m.id,
1593
+ label: m.name,
1594
+ hint: m.reasoning ? "reasoning" : m.status === "beta" ? "beta" : void 0
1595
+ }));
1596
+ const currentModel = existingConfig.defaultModel?.split("/")[1];
1597
+ const modelResult = await p.select({
1598
+ message: "Select your default model",
1599
+ options: modelOptions,
1600
+ initialValue: currentModel,
1601
+ maxItems: MAX_SELECT_ITEMS
1602
+ });
1603
+ if (p.isCancel(modelResult)) {
1604
+ p.outro("Setup cancelled");
1605
+ return {
1606
+ success: false,
1607
+ configPath
1608
+ };
1609
+ }
1610
+ model = modelResult;
1611
+ }
1612
+ const detectedAgents = detectInstalledAgents();
1613
+ const allAgentConfigs = getAllAgentConfigs();
1614
+ if (detectedAgents.length > 0) {
1615
+ const detectedNames = detectedAgents.map((a) => allAgentConfigs.find((c) => c.name === a)?.displayName ?? a).join(", ");
1616
+ p.log.info(`Detected agents: ${detectedNames}`);
1617
+ }
1618
+ const agentOptions = allAgentConfigs.map((config) => ({
1619
+ value: config.name,
1620
+ label: config.displayName,
1621
+ hint: config.globalSkillsDir
1622
+ }));
1623
+ let agents;
1624
+ if (options.agents) {
1625
+ const agentNames = options.agents.split(",").map((a) => a.trim());
1626
+ const validAgents = allAgentConfigs.filter((c) => agentNames.includes(c.name)).map((c) => c.name);
1627
+ if (validAgents.length === 0) {
1628
+ p.log.error("No valid agent names provided");
1629
+ p.outro("Setup failed");
1630
+ return {
1631
+ success: false,
1632
+ configPath
1633
+ };
1634
+ }
1635
+ agents = validAgents;
1636
+ } else {
1637
+ const initialAgents = existingConfig.agents && existingConfig.agents.length > 0 ? existingConfig.agents : detectedAgents;
1638
+ const agentsResult = await p.multiselect({
1639
+ message: "Select agents to install skills to",
1640
+ options: agentOptions,
1641
+ initialValues: initialAgents,
1642
+ required: false
1643
+ });
1644
+ if (p.isCancel(agentsResult)) {
1645
+ p.outro("Setup cancelled");
1646
+ return {
1647
+ success: false,
1648
+ configPath
1649
+ };
1650
+ }
1651
+ agents = agentsResult;
1652
+ }
1653
+ const defaultModel = `${provider}/${model}`;
1654
+ const newConfig = {
1655
+ repoRoot,
1656
+ defaultModel,
1657
+ agents
1658
+ };
1659
+ try {
1660
+ saveConfig(newConfig);
1661
+ installGlobalSkill();
1662
+ p.log.success("Configuration saved!");
1663
+ p.log.info(` Config file: ${configPath}`);
1664
+ p.log.info(` Repo root: ${repoRoot}`);
1665
+ p.log.info(` Meta root: ${getMetaRoot()}`);
1666
+ p.log.info(` State root: ${Paths.state}`);
1667
+ p.log.info(` Model: ${defaultModel}`);
1668
+ p.log.info(` Agents: ${agents.join(", ")}`);
1669
+ } catch (error) {
1670
+ const message = error instanceof Error ? error.message : "Unknown error";
1671
+ p.log.error(`Failed to save configuration: ${message}`);
1672
+ p.outro("Setup failed");
1673
+ return {
1674
+ success: false,
1675
+ configPath
1676
+ };
1677
+ }
1678
+ const expandedRepoRoot = expandTilde(repoRoot);
1679
+ if (existsSync(expandedRepoRoot)) {
1680
+ const previewResult = await discoverRepos({
1681
+ repoRoot: expandedRepoRoot,
1682
+ dryRun: true
1683
+ });
1684
+ if (previewResult.discovered.length > 0) {
1685
+ p.log.info("");
1686
+ p.log.info(`Found ${previewResult.discovered.length} existing repos in ${repoRoot}`);
1687
+ let shouldDiscover = options.yes;
1688
+ if (!options.yes) {
1689
+ const confirmDiscover = await p.confirm({
1690
+ message: "Add them to your clone map? (they will be marked as not referenced)",
1691
+ initialValue: true
1692
+ });
1693
+ if (!p.isCancel(confirmDiscover)) shouldDiscover = confirmDiscover;
1694
+ }
1695
+ if (shouldDiscover) {
1696
+ const result = await discoverRepos({ repoRoot: expandedRepoRoot });
1697
+ p.log.success(`Added ${result.discovered.length} repos to clone map`);
1698
+ }
1699
+ }
1700
+ }
1701
+ p.outro("Setup complete. Run 'ow pull <repo>' to get started.");
1702
+ return {
1703
+ success: true,
1704
+ configPath
1705
+ };
1706
+ }
1707
+
1708
+ //#endregion
1709
+ //#region src/handlers/project.ts
1710
+ function detectProjectRoot() {
1711
+ let currentDir = process.cwd();
1712
+ while (currentDir !== homedir()) {
1713
+ if (existsSync(join(currentDir, ".git"))) return currentDir;
1714
+ const parent = dirname(currentDir);
1715
+ if (parent === currentDir) break;
1716
+ currentDir = parent;
1717
+ }
1718
+ return null;
1719
+ }
1720
+ async function projectInitHandler(options = {}) {
1721
+ p.intro("ow project init");
1722
+ if (!existsSync(getConfigPath())) {
1723
+ p.log.error("No global config found. Run 'ow init' first to set up global configuration.");
1724
+ p.outro("");
1725
+ return {
1726
+ success: false,
1727
+ message: "No global config found"
1728
+ };
1729
+ }
1730
+ if (!loadConfig().defaultModel) {
1731
+ p.log.error("Global config is missing AI settings. Run 'ow init --force' to reconfigure.");
1732
+ p.outro("");
1733
+ return {
1734
+ success: false,
1735
+ message: "Invalid global config"
1736
+ };
1737
+ }
1738
+ const projectRoot = detectProjectRoot() || process.cwd();
1739
+ p.log.info(`Project root: ${projectRoot}`);
1740
+ p.log.step("Scanning dependencies...");
1741
+ const dependencies = parseDependencies(projectRoot);
1742
+ if (dependencies.length === 0) {
1743
+ p.log.warn("No dependencies found in manifest files.");
1744
+ p.outro("");
1745
+ return {
1746
+ success: true,
1747
+ message: "No dependencies found"
1748
+ };
1749
+ }
1750
+ p.log.info(`Found ${dependencies.length} dependencies`);
1751
+ p.log.step("Resolving GitHub repositories...");
1752
+ const resolvedPromises = dependencies.map((dep) => resolveDependencyRepo(dep.name));
1753
+ const resolved = await Promise.all(resolvedPromises);
1754
+ const skipList = options.skip ? options.skip.split(",").map((d) => d.trim()) : [];
1755
+ const depsList = options.deps ? options.deps.split(",").map((d) => d.trim()) : [];
1756
+ let filtered = resolved.filter((r) => {
1757
+ if (r.source === "unknown") return false;
1758
+ if (skipList.includes(r.dep)) return false;
1759
+ if (depsList.length > 0) return depsList.includes(r.dep);
1760
+ return true;
1761
+ });
1762
+ if (filtered.length === 0) {
1763
+ p.log.warn("No resolvable dependencies after filtering.");
1764
+ p.outro("");
1765
+ return {
1766
+ success: true,
1767
+ message: "No resolvable dependencies"
1768
+ };
1769
+ }
1770
+ const matches = matchDependenciesToReferences(filtered);
1771
+ let selected;
1772
+ if (options.all || options.deps) selected = matches;
1773
+ else {
1774
+ const checklistOptions = matches.map((m) => {
1775
+ return {
1776
+ value: m,
1777
+ label: `${m.dep} (${m.repo}) - ${m.status}`,
1778
+ hint: m.status
1779
+ };
1780
+ });
1781
+ const selectedResult = await p.multiselect({
1782
+ message: "Select dependencies to install references for:",
1783
+ options: checklistOptions,
1784
+ required: false
1785
+ });
1786
+ if (p.isCancel(selectedResult)) {
1787
+ p.cancel("Operation cancelled");
1788
+ return {
1789
+ success: false,
1790
+ message: "Cancelled by user"
1791
+ };
1792
+ }
1793
+ selected = selectedResult;
1794
+ }
1795
+ if (selected.length === 0) {
1796
+ p.log.warn("No dependencies selected.");
1797
+ p.outro("");
1798
+ return {
1799
+ success: true,
1800
+ message: "No dependencies selected"
1801
+ };
1802
+ }
1803
+ if (!options.yes && !options.dryRun) {
1804
+ const confirm = await p.confirm({ message: `Install references for ${selected.length} dependencies?` });
1805
+ if (p.isCancel(confirm) || !confirm) {
1806
+ p.cancel("Operation cancelled");
1807
+ return {
1808
+ success: false,
1809
+ message: "Cancelled by user"
1810
+ };
1811
+ }
1812
+ }
1813
+ if (options.dryRun) {
1814
+ p.log.info("");
1815
+ p.log.info("Dry run - would install references for:");
1816
+ for (const match of selected) p.log.info(` - ${match.dep} (${match.repo})`);
1817
+ p.outro("Dry run complete");
1818
+ return {
1819
+ success: true,
1820
+ message: "Dry run complete"
1821
+ };
1822
+ }
1823
+ p.log.step(`Installing ${selected.length} references...`);
1824
+ const installed = [];
1825
+ let failedCount = 0;
1826
+ for (const match of selected) try {
1827
+ if (!match.repo) continue;
1828
+ p.log.info(`Installing reference for ${match.dep}...`);
1829
+ const pullResult = await pullHandler({
1830
+ repo: match.repo,
1831
+ shallow: true,
1832
+ force: options.generate,
1833
+ verbose: false
1834
+ });
1835
+ if (pullResult.success && pullResult.referenceInstalled) {
1836
+ const referencePath = getReferencePath(match.repo);
1837
+ installed.push({
1838
+ dependency: match.dep,
1839
+ reference: toReferenceFileName(match.repo),
1840
+ path: referencePath
1841
+ });
1842
+ } else {
1843
+ p.log.warn(`Failed to install reference for ${match.dep}`);
1844
+ failedCount++;
1845
+ }
1846
+ } catch (error) {
1847
+ const errMsg = error instanceof Error ? error.message : "Unknown error";
1848
+ p.log.error(`Error installing ${match.dep}: ${errMsg}`);
1849
+ failedCount++;
1850
+ }
1851
+ const map = readGlobalMap();
1852
+ writeProjectMap(projectRoot, Object.fromEntries(selected.filter((m) => m.repo).map((m) => {
1853
+ const qualifiedName = `github.com:${m.repo}`;
1854
+ const entry = map.repos[qualifiedName];
1855
+ return [qualifiedName, {
1856
+ localPath: entry?.localPath ?? "",
1857
+ reference: toReferenceFileName(m.repo),
1858
+ keywords: entry?.keywords ?? []
1859
+ }];
1860
+ })));
1861
+ if (installed.length > 0) {
1862
+ p.log.step("Updating AGENTS.md...");
1863
+ try {
1864
+ updateAgentFiles(projectRoot, installed);
1865
+ p.log.success("AGENTS.md updated");
1866
+ } catch (error) {
1867
+ const errMsg = error instanceof Error ? error.message : "Unknown error";
1868
+ p.log.error(`Failed to update AGENTS.md: ${errMsg}`);
1869
+ }
1870
+ }
1871
+ p.log.info("");
1872
+ p.log.success(`Installed ${installed.length} references`);
1873
+ if (failedCount > 0) p.log.warn(`Failed to install ${failedCount} references`);
1874
+ p.outro("Project init complete");
1875
+ return {
1876
+ success: true,
1877
+ referencesInstalled: installed.length
1878
+ };
1879
+ }
1880
+
1881
+ //#endregion
1882
+ //#region src/handlers/upgrade.ts
1883
+ /**
1884
+ * Upgrade command handler
1885
+ */
1886
+ async function upgradeHandler(options) {
1887
+ const { target, method: methodOverride } = options;
1888
+ try {
1889
+ const s = createSpinner();
1890
+ s.start("Detecting installation method...");
1891
+ const method = methodOverride ?? detectInstallMethod();
1892
+ s.stop(`Installation method: ${method}`);
1893
+ if (method === "unknown") {
1894
+ const confirm = await p.confirm({
1895
+ message: "Could not detect installation method. Attempt curl-based upgrade?",
1896
+ initialValue: false
1897
+ });
1898
+ if (p.isCancel(confirm) || !confirm) {
1899
+ p.log.info("Aborted.");
1900
+ return {
1901
+ success: false,
1902
+ message: "Unknown installation method"
1903
+ };
1904
+ }
1905
+ }
1906
+ const effectiveMethod = method === "unknown" ? "curl" : method;
1907
+ const currentVersion = getCurrentVersion();
1908
+ p.log.info(`Current version: ${currentVersion}`);
1909
+ let targetVersion = target;
1910
+ if (!targetVersion) {
1911
+ s.start("Fetching latest version...");
1912
+ const latest = await fetchLatestVersion(effectiveMethod);
1913
+ s.stop(latest ? `Latest version: ${latest}` : "Could not fetch latest version");
1914
+ if (!latest) {
1915
+ p.log.error("Failed to fetch latest version");
1916
+ return {
1917
+ success: false,
1918
+ message: "Failed to fetch latest version"
1919
+ };
1920
+ }
1921
+ targetVersion = latest;
1922
+ }
1923
+ targetVersion = targetVersion.replace(/^v/, "");
1924
+ if (currentVersion === targetVersion) {
1925
+ p.log.success(`Already on version ${targetVersion}`);
1926
+ return {
1927
+ success: true,
1928
+ from: currentVersion,
1929
+ to: targetVersion
1930
+ };
1931
+ }
1932
+ p.log.info(`Upgrading from ${currentVersion} to ${targetVersion}...`);
1933
+ await executeUpgrade(effectiveMethod, targetVersion);
1934
+ p.log.success(`Successfully upgraded to ${targetVersion}`);
1935
+ p.log.info("Restart your terminal or run 'ow --version' to verify.");
1936
+ return {
1937
+ success: true,
1938
+ from: currentVersion,
1939
+ to: targetVersion
1940
+ };
1941
+ } catch (error) {
1942
+ const message = error instanceof Error ? error.message : "Unknown error";
1943
+ p.log.error(message);
1944
+ return {
1945
+ success: false,
1946
+ message
1947
+ };
1948
+ }
1949
+ }
1950
+
1951
+ //#endregion
1952
+ //#region src/handlers/uninstall.ts
1953
+ /**
1954
+ * Uninstall command handler
1955
+ */
1956
+ async function uninstallHandler(options) {
1957
+ const { keepConfig = false, keepData = false, dryRun = false, force = false } = options;
1958
+ try {
1959
+ const method = detectInstallMethod();
1960
+ p.log.info(`Installation method: ${method}`);
1961
+ const toRemove = [
1962
+ {
1963
+ path: Paths.data,
1964
+ label: "Data",
1965
+ keep: keepData
1966
+ },
1967
+ {
1968
+ path: Paths.config,
1969
+ label: "Config",
1970
+ keep: keepConfig
1971
+ },
1972
+ {
1973
+ path: Paths.state,
1974
+ label: "State",
1975
+ keep: false
1976
+ }
1977
+ ].filter((d) => !d.keep && existsSync(d.path));
1978
+ if (dryRun || !force) {
1979
+ p.log.info("The following will be removed:");
1980
+ console.log("");
1981
+ if (method === "curl") console.log(" Binary: ~/.local/bin/ow");
1982
+ else console.log(` Package: offworld (via ${method})`);
1983
+ for (const dir of toRemove) console.log(` ${dir.label}: ${dir.path}`);
1984
+ if (keepConfig) console.log(` [kept] Config: ${Paths.config}`);
1985
+ if (keepData) console.log(` [kept] Data: ${Paths.data}`);
1986
+ console.log("");
1987
+ }
1988
+ if (dryRun) {
1989
+ p.log.info("Dry run - nothing was removed.");
1990
+ return {
1991
+ success: true,
1992
+ removed: {
1993
+ binary: method === "curl",
1994
+ config: !keepConfig && existsSync(Paths.config),
1995
+ data: !keepData && existsSync(Paths.data),
1996
+ state: existsSync(Paths.state)
1997
+ }
1998
+ };
1999
+ }
2000
+ if (!force) {
2001
+ const confirm = await p.confirm({
2002
+ message: "Are you sure you want to uninstall offworld?",
2003
+ initialValue: false
2004
+ });
2005
+ if (p.isCancel(confirm) || !confirm) {
2006
+ p.log.info("Aborted.");
2007
+ return {
2008
+ success: false,
2009
+ message: "Aborted by user"
2010
+ };
2011
+ }
2012
+ }
2013
+ const s = createSpinner();
2014
+ const removed = {};
2015
+ s.start("Removing offworld...");
2016
+ try {
2017
+ await executeUninstall(method);
2018
+ removed.binary = true;
2019
+ s.stop("Removed offworld binary/package");
2020
+ } catch (err) {
2021
+ s.stop("Failed to remove binary/package");
2022
+ p.log.warn(err instanceof Error ? err.message : "Unknown error");
2023
+ }
2024
+ for (const dir of toRemove) {
2025
+ s.start(`Removing ${dir.label.toLowerCase()}...`);
2026
+ try {
2027
+ rmSync(dir.path, {
2028
+ recursive: true,
2029
+ force: true
2030
+ });
2031
+ s.stop(`Removed ${dir.label.toLowerCase()}`);
2032
+ if (dir.label === "Config") removed.config = true;
2033
+ if (dir.label === "Data") removed.data = true;
2034
+ if (dir.label === "State") removed.state = true;
2035
+ } catch (err) {
2036
+ s.stop(`Failed to remove ${dir.label.toLowerCase()}`);
2037
+ p.log.warn(err instanceof Error ? err.message : "Unknown error");
2038
+ }
2039
+ }
2040
+ if (method === "curl") {
2041
+ const shellConfigs = getShellConfigFiles();
2042
+ const cleaned = [];
2043
+ for (const config of shellConfigs) if (cleanShellConfig(config)) cleaned.push(config);
2044
+ if (cleaned.length > 0) {
2045
+ p.log.info(`Cleaned PATH from: ${cleaned.join(", ")}`);
2046
+ removed.shellConfigs = cleaned;
2047
+ }
2048
+ }
2049
+ p.log.success("Uninstall complete!");
2050
+ if (method === "curl") p.log.info("You may need to restart your terminal.");
2051
+ return {
2052
+ success: true,
2053
+ removed
2054
+ };
2055
+ } catch (error) {
2056
+ const message = error instanceof Error ? error.message : "Unknown error";
2057
+ p.log.error(message);
2058
+ return {
2059
+ success: false,
2060
+ message
2061
+ };
2062
+ }
2063
+ }
2064
+
2065
+ //#endregion
2066
+ //#region src/handlers/map.ts
2067
+ /**
2068
+ * Map command handlers for fast repo routing
2069
+ */
2070
+ async function mapShowHandler(options) {
2071
+ const { repo, json, path, ref } = options;
2072
+ const result = getMapEntry(repo);
2073
+ if (!result) {
2074
+ if (json) console.log(JSON.stringify({ found: false }));
2075
+ else if (!path && !ref) p.log.error(`Repo not found: ${repo}`);
2076
+ return { found: false };
2077
+ }
2078
+ const { scope, qualifiedName, entry } = result;
2079
+ const primary = "primary" in entry ? entry.primary : entry.reference;
2080
+ const keywords = entry.keywords ?? [];
2081
+ const refPath = `${Paths.offworldReferencesDir}/${primary}`;
2082
+ if (path) {
2083
+ console.log(entry.localPath);
2084
+ return {
2085
+ found: true,
2086
+ scope,
2087
+ qualifiedName,
2088
+ localPath: entry.localPath,
2089
+ primary,
2090
+ keywords
2091
+ };
2092
+ }
2093
+ if (ref) {
2094
+ console.log(refPath);
2095
+ return {
2096
+ found: true,
2097
+ scope,
2098
+ qualifiedName,
2099
+ localPath: entry.localPath,
2100
+ primary,
2101
+ keywords
2102
+ };
2103
+ }
2104
+ if (json) console.log(JSON.stringify({
2105
+ found: true,
2106
+ scope,
2107
+ qualifiedName,
2108
+ localPath: entry.localPath,
2109
+ primary,
2110
+ referencePath: refPath,
2111
+ keywords
2112
+ }, null, 2));
2113
+ else {
2114
+ console.log(`Repo: ${qualifiedName}`);
2115
+ console.log(`Scope: ${scope}`);
2116
+ console.log(`Path: ${entry.localPath}`);
2117
+ console.log(`Reference: ${refPath}`);
2118
+ if (keywords.length > 0) console.log(`Keywords: ${keywords.join(", ")}`);
2119
+ }
2120
+ return {
2121
+ found: true,
2122
+ scope,
2123
+ qualifiedName,
2124
+ localPath: entry.localPath,
2125
+ primary,
2126
+ keywords
2127
+ };
2128
+ }
2129
+ async function mapSearchHandler(options) {
2130
+ const { term, limit = 10, json } = options;
2131
+ const results = searchMap(term, { limit });
2132
+ if (json) console.log(JSON.stringify(results, null, 2));
2133
+ else if (results.length === 0) p.log.warn(`No matches found for: ${term}`);
2134
+ else for (const r of results) {
2135
+ const refPath = `${Paths.offworldReferencesDir}/${r.primary}`;
2136
+ console.log(`${r.fullName}`);
2137
+ console.log(` path: ${r.localPath}`);
2138
+ console.log(` ref: ${refPath}`);
2139
+ if (r.keywords.length > 0) console.log(` keywords: ${r.keywords.join(", ")}`);
2140
+ console.log("");
2141
+ }
2142
+ return { results };
2143
+ }
2144
+
2145
+ //#endregion
2146
+ //#region src/index.ts
2147
+ const version = "0.1.0";
2148
+ const router = os.router({
2149
+ pull: os.input(z.object({
2150
+ repo: z.string().describe("repo").meta({ positional: true }),
2151
+ reference: z.string().optional().describe("Reference name to pull (defaults to owner-repo)").meta({ alias: "r" }),
2152
+ shallow: z.boolean().default(false).describe("Use shallow clone (--depth 1)").meta({ negativeAlias: "full-history" }),
2153
+ sparse: z.boolean().default(false).describe("Use sparse checkout (only src/, lib/, packages/, docs/)"),
2154
+ branch: z.string().optional().describe("Branch to clone"),
2155
+ force: z.boolean().default(false).describe("Force re-generation").meta({ alias: "f" }),
2156
+ verbose: z.boolean().default(false).describe("Show detailed output"),
2157
+ model: z.string().optional().describe("Model override (provider/model)").meta({ alias: "m" })
2158
+ })).meta({
2159
+ description: "Clone a repository and fetch or generate its reference",
2160
+ negateBooleans: true
2161
+ }).handler(async ({ input }) => {
2162
+ await pullHandler({
2163
+ repo: input.repo,
2164
+ reference: input.reference,
2165
+ shallow: input.shallow,
2166
+ sparse: input.sparse,
2167
+ branch: input.branch,
2168
+ force: input.force,
2169
+ verbose: input.verbose,
2170
+ model: input.model
2171
+ });
2172
+ }),
2173
+ list: os.input(z.object({
2174
+ json: z.boolean().default(false).describe("Output as JSON"),
2175
+ paths: z.boolean().default(false).describe("Show full paths"),
2176
+ pattern: z.string().optional().describe("Filter by pattern (e.g. 'react-*')")
2177
+ })).meta({
2178
+ description: "List managed repositories (alias for 'ow repo list')",
2179
+ aliases: { command: ["ls"] }
2180
+ }).handler(async ({ input }) => {
2181
+ await repoListHandler({
2182
+ json: input.json,
2183
+ paths: input.paths,
2184
+ pattern: input.pattern
2185
+ });
2186
+ }),
2187
+ generate: os.input(z.object({
2188
+ repo: z.string().describe("repo").meta({ positional: true }),
2189
+ force: z.boolean().default(false).describe("Force even if remote exists").meta({ alias: "f" }),
2190
+ model: z.string().optional().describe("Model override (provider/model)").meta({ alias: "m" })
2191
+ })).meta({
2192
+ description: "Generate reference locally (ignores remote)",
2193
+ aliases: { command: ["gen"] }
2194
+ }).handler(async ({ input }) => {
2195
+ await generateHandler({
2196
+ repo: input.repo,
2197
+ force: input.force,
2198
+ model: input.model
2199
+ });
2200
+ }),
2201
+ push: os.input(z.object({ repo: z.string().describe("repo").meta({ positional: true }) })).meta({ description: "Push local reference to offworld.sh" }).handler(async ({ input }) => {
2202
+ await pushHandler({ repo: input.repo });
2203
+ }),
2204
+ remove: os.input(z.object({
2205
+ repo: z.string().describe("repo").meta({ positional: true }),
2206
+ yes: z.boolean().default(false).describe("Skip confirmation").meta({ alias: "y" }),
2207
+ referenceOnly: z.boolean().default(false).describe("Only remove reference files (keep repo)"),
2208
+ repoOnly: z.boolean().default(false).describe("Only remove cloned repo (keep reference)"),
2209
+ dryRun: z.boolean().default(false).describe("Show what would be done").meta({ alias: "d" })
2210
+ })).meta({
2211
+ description: "Remove a cloned repository and its reference",
2212
+ aliases: { command: ["rm"] }
2213
+ }).handler(async ({ input }) => {
2214
+ await rmHandler({
2215
+ repo: input.repo,
2216
+ yes: input.yes,
2217
+ referenceOnly: input.referenceOnly,
2218
+ repoOnly: input.repoOnly,
2219
+ dryRun: input.dryRun
2220
+ });
2221
+ }),
2222
+ auth: os.router({
2223
+ login: os.input(z.object({})).meta({ description: "Login to offworld.sh" }).handler(async () => {
2224
+ await authLoginHandler();
2225
+ }),
2226
+ logout: os.input(z.object({})).meta({ description: "Logout from offworld.sh" }).handler(async () => {
2227
+ await authLogoutHandler();
2228
+ }),
2229
+ status: os.input(z.object({})).meta({ description: "Show authentication status" }).handler(async () => {
2230
+ await authStatusHandler();
2231
+ })
2232
+ }),
2233
+ config: os.router({
2234
+ show: os.input(z.object({ json: z.boolean().default(false).describe("Output as JSON") })).meta({
2235
+ description: "Show all config settings",
2236
+ default: true
2237
+ }).handler(async ({ input }) => {
2238
+ await configShowHandler({ json: input.json });
2239
+ }),
2240
+ set: os.input(z.object({
2241
+ key: z.string().describe("key").meta({ positional: true }),
2242
+ value: z.string().describe("value").meta({ positional: true })
2243
+ })).meta({ description: `Set a config value
2244
+
2245
+ Valid keys:
2246
+ repoRoot (string) Where to clone repos (e.g., ~/ow)
2247
+ defaultShallow (boolean) Use shallow clone by default (true/false)
2248
+ defaultModel (string) AI provider/model (e.g., anthropic/claude-sonnet-4-20250514)
2249
+ agents (list) Comma-separated agents (e.g., claude-code,opencode)` }).handler(async ({ input }) => {
2250
+ await configSetHandler({
2251
+ key: input.key,
2252
+ value: input.value
2253
+ });
2254
+ }),
2255
+ get: os.input(z.object({ key: z.string().describe("key").meta({ positional: true }) })).meta({ description: `Get a config value
2256
+
2257
+ Valid keys: repoRoot, defaultShallow, defaultModel, agents` }).handler(async ({ input }) => {
2258
+ await configGetHandler({ key: input.key });
2259
+ }),
2260
+ reset: os.input(z.object({})).meta({ description: "Reset config to defaults" }).handler(async () => {
2261
+ await configResetHandler();
2262
+ }),
2263
+ path: os.input(z.object({})).meta({ description: "Show config file location" }).handler(async () => {
2264
+ await configPathHandler();
2265
+ }),
2266
+ agents: os.input(z.object({})).meta({ description: "Interactively select agents for reference installation" }).handler(async () => {
2267
+ await configAgentsHandler();
2268
+ })
2269
+ }),
2270
+ init: os.input(z.object({
2271
+ yes: z.boolean().default(false).describe("Skip confirmation prompts").meta({ alias: "y" }),
2272
+ force: z.boolean().default(false).describe("Reconfigure even if config exists").meta({ alias: "f" }),
2273
+ model: z.string().optional().describe("AI provider/model (e.g., anthropic/claude-sonnet-4-20250514)").meta({ alias: "m" }),
2274
+ repoRoot: z.string().optional().describe("Where to clone repos"),
2275
+ agents: z.string().optional().describe("Comma-separated agents").meta({ alias: "a" })
2276
+ })).meta({ description: "Initialize configuration with interactive setup" }).handler(async ({ input }) => {
2277
+ await initHandler({
2278
+ yes: input.yes,
2279
+ force: input.force,
2280
+ model: input.model,
2281
+ repoRoot: input.repoRoot,
2282
+ agents: input.agents
2283
+ });
2284
+ }),
2285
+ project: os.router({ init: os.input(z.object({
2286
+ all: z.boolean().default(false).describe("Select all detected dependencies"),
2287
+ deps: z.string().optional().describe("Comma-separated deps to include (skip selection)"),
2288
+ skip: z.string().optional().describe("Comma-separated deps to exclude"),
2289
+ generate: z.boolean().default(false).describe("Generate references for deps without existing ones").meta({ alias: "g" }),
2290
+ dryRun: z.boolean().default(false).describe("Show what would be done without doing it").meta({ alias: "d" }),
2291
+ yes: z.boolean().default(false).describe("Skip confirmations").meta({ alias: "y" })
2292
+ })).meta({
2293
+ description: "Scan manifest, install references, update AGENTS.md",
2294
+ default: true
2295
+ }).handler(async ({ input }) => {
2296
+ await projectInitHandler({
2297
+ all: input.all,
2298
+ deps: input.deps,
2299
+ skip: input.skip,
2300
+ generate: input.generate,
2301
+ dryRun: input.dryRun,
2302
+ yes: input.yes
2303
+ });
2304
+ }) }),
2305
+ map: os.router({
2306
+ show: os.input(z.object({
2307
+ repo: z.string().describe("repo").meta({ positional: true }),
2308
+ json: z.boolean().default(false).describe("Output as JSON"),
2309
+ path: z.boolean().default(false).describe("Print only local path"),
2310
+ ref: z.boolean().default(false).describe("Print only reference file path")
2311
+ })).meta({
2312
+ description: "Show map entry for a repo",
2313
+ default: true
2314
+ }).handler(async ({ input }) => {
2315
+ await mapShowHandler({
2316
+ repo: input.repo,
2317
+ json: input.json,
2318
+ path: input.path,
2319
+ ref: input.ref
2320
+ });
2321
+ }),
2322
+ search: os.input(z.object({
2323
+ term: z.string().describe("term").meta({ positional: true }),
2324
+ limit: z.number().default(10).describe("Max results").meta({ alias: "n" }),
2325
+ json: z.boolean().default(false).describe("Output as JSON")
2326
+ })).meta({ description: "Search map for repos matching a term" }).handler(async ({ input }) => {
2327
+ await mapSearchHandler({
2328
+ term: input.term,
2329
+ limit: input.limit,
2330
+ json: input.json
2331
+ });
2332
+ })
2333
+ }),
2334
+ repo: os.router({
2335
+ list: os.input(z.object({
2336
+ json: z.boolean().default(false).describe("Output as JSON"),
2337
+ paths: z.boolean().default(false).describe("Show full paths"),
2338
+ pattern: z.string().optional().describe("Filter by pattern (e.g. 'react-*')")
2339
+ })).meta({
2340
+ description: "List managed repositories",
2341
+ default: true,
2342
+ aliases: { command: ["ls"] }
2343
+ }).handler(async ({ input }) => {
2344
+ await repoListHandler({
2345
+ json: input.json,
2346
+ paths: input.paths,
2347
+ pattern: input.pattern
2348
+ });
2349
+ }),
2350
+ update: os.input(z.object({
2351
+ all: z.boolean().default(false).describe("Update all repos"),
2352
+ pattern: z.string().optional().describe("Filter by pattern"),
2353
+ dryRun: z.boolean().default(false).describe("Show what would be updated").meta({ alias: "d" }),
2354
+ unshallow: z.boolean().default(false).describe("Convert shallow clones to full clones")
2355
+ })).meta({ description: "Update repos (git fetch + pull)" }).handler(async ({ input }) => {
2356
+ await repoUpdateHandler({
2357
+ all: input.all,
2358
+ pattern: input.pattern,
2359
+ dryRun: input.dryRun,
2360
+ unshallow: input.unshallow
2361
+ });
2362
+ }),
2363
+ prune: os.input(z.object({
2364
+ dryRun: z.boolean().default(false).describe("Show what would be pruned").meta({ alias: "d" }),
2365
+ yes: z.boolean().default(false).describe("Skip confirmation").meta({ alias: "y" }),
2366
+ removeOrphans: z.boolean().default(false).describe("Also remove orphaned directories")
2367
+ })).meta({ description: "Remove stale index entries and find orphaned directories" }).handler(async ({ input }) => {
2368
+ await repoPruneHandler({
2369
+ dryRun: input.dryRun,
2370
+ yes: input.yes,
2371
+ removeOrphans: input.removeOrphans
2372
+ });
2373
+ }),
2374
+ status: os.input(z.object({ json: z.boolean().default(false).describe("Output as JSON") })).meta({ description: "Show summary of managed repos" }).handler(async ({ input }) => {
2375
+ await repoStatusHandler({ json: input.json });
2376
+ }),
2377
+ gc: os.input(z.object({
2378
+ olderThan: z.string().optional().describe("Remove repos not accessed in N days (e.g. '30d')"),
2379
+ withoutReference: z.boolean().default(false).describe("Remove repos without references"),
2380
+ dryRun: z.boolean().default(false).describe("Show what would be removed").meta({ alias: "d" }),
2381
+ yes: z.boolean().default(false).describe("Skip confirmation").meta({ alias: "y" })
2382
+ })).meta({ description: "Garbage collect old/unused repos" }).handler(async ({ input }) => {
2383
+ await repoGcHandler({
2384
+ olderThan: input.olderThan,
2385
+ withoutReference: input.withoutReference,
2386
+ dryRun: input.dryRun,
2387
+ yes: input.yes
2388
+ });
2389
+ }),
2390
+ discover: os.input(z.object({
2391
+ dryRun: z.boolean().default(false).describe("Show what would be added").meta({ alias: "d" }),
2392
+ yes: z.boolean().default(false).describe("Skip confirmation").meta({ alias: "y" })
2393
+ })).meta({ description: "Discover and index existing repos in repoRoot" }).handler(async ({ input }) => {
2394
+ await repoDiscoverHandler({
2395
+ dryRun: input.dryRun,
2396
+ yes: input.yes
2397
+ });
2398
+ })
2399
+ }),
2400
+ upgrade: os.input(z.object({ target: z.string().optional().describe("Version to upgrade to") })).meta({ description: "Upgrade offworld to latest or specific version" }).handler(async ({ input }) => {
2401
+ await upgradeHandler({ target: input.target });
2402
+ }),
2403
+ uninstall: os.input(z.object({
2404
+ keepConfig: z.boolean().default(false).describe("Keep configuration files"),
2405
+ keepData: z.boolean().default(false).describe("Keep data files (references, repos)"),
2406
+ dryRun: z.boolean().default(false).describe("Show what would be removed").meta({ alias: "d" }),
2407
+ force: z.boolean().default(false).describe("Skip confirmation").meta({ alias: "f" })
2408
+ })).meta({ description: "Uninstall offworld and remove related files" }).handler(async ({ input }) => {
2409
+ await uninstallHandler({
2410
+ keepConfig: input.keepConfig,
2411
+ keepData: input.keepData,
2412
+ dryRun: input.dryRun,
2413
+ force: input.force
2414
+ });
2415
+ })
2416
+ });
2417
+ const stripSecondaryDescription = (help) => help.split("\n").filter((line) => !line.startsWith("Available subcommands:")).join("\n");
2418
+ const stripDefaultCommandHelp = (help) => {
2419
+ const firstUsageIndex = help.indexOf("Usage:");
2420
+ if (firstUsageIndex === -1) return help.trimEnd();
2421
+ const secondUsageIndex = help.indexOf("Usage:", firstUsageIndex + 1);
2422
+ if (secondUsageIndex === -1) return help.trimEnd();
2423
+ return help.slice(0, secondUsageIndex).trimEnd();
2424
+ };
2425
+ const normalizeRootHelp = (help) => stripDefaultCommandHelp(stripSecondaryDescription(help));
2426
+ function createOwCli() {
2427
+ const cli = createCli({
2428
+ router,
2429
+ description: "Offworld CLI - Repository reference generation for AI coding agents"
2430
+ });
2431
+ const buildProgram = (runParams) => {
2432
+ const program = cli.buildProgram(runParams);
2433
+ const originalHelpInformation = program.helpInformation.bind(program);
2434
+ program.helpInformation = () => normalizeRootHelp(originalHelpInformation());
2435
+ return program;
2436
+ };
2437
+ const run = (runParams, program) => cli.run(runParams, program ?? buildProgram(runParams));
2438
+ return {
2439
+ ...cli,
2440
+ buildProgram,
2441
+ run
2442
+ };
2443
+ }
2444
+
2445
+ //#endregion
2446
+ export { router as n, version as r, createOwCli as t };
2447
+ //# sourceMappingURL=src-CZHUGu1Q.mjs.map