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.
- package/README.md +187 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +54 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +137 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/src-CZHUGu1Q.mjs +2447 -0
- package/dist/src-CZHUGu1Q.mjs.map +1 -0
- package/package.json +71 -0
|
@@ -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
|