@schoolai/shipyard 3.3.0 → 3.3.1-nightly.20260427.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ TIMEOUT_MS,
4
+ run,
5
+ runWithTimeout
6
+ } from "./chunk-KYLY4DJF.js";
7
+ import {
8
+ logger
9
+ } from "./chunk-DNIC3FOH.js";
10
+ import {
11
+ external_exports,
12
+ getShipyardHome
13
+ } from "./chunk-M5M6VC5F.js";
14
+
15
+ // src/shared/capabilities/git-repo.ts
16
+ import { readdir } from "fs/promises";
17
+ import { basename, join as join3 } from "path";
18
+
19
+ // src/shared/capabilities/environments-cache.ts
20
+ import { mkdir, readFile, rename, stat, unlink, writeFile } from "fs/promises";
21
+ import { dirname as dirname2, isAbsolute, join as join2, resolve } from "path";
22
+
23
+ // src/shared/fs-utils.ts
24
+ import { existsSync } from "fs";
25
+ import { dirname, join, parse } from "path";
26
+ function isEnoent(err) {
27
+ return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
28
+ }
29
+ function isEexist(err) {
30
+ return typeof err === "object" && err !== null && "code" in err && err.code === "EEXIST";
31
+ }
32
+ function isCorruptionError(err) {
33
+ if (err instanceof SyntaxError) return true;
34
+ if (typeof err === "object" && err !== null && "name" in err && err.name === "ZodError")
35
+ return true;
36
+ return false;
37
+ }
38
+ function findProjectRoot(startDir) {
39
+ let dir = startDir;
40
+ while (true) {
41
+ if (existsSync(join(dir, "pnpm-workspace.yaml")) || existsSync(join(dir, ".git"))) {
42
+ return dir;
43
+ }
44
+ const parent = dirname(dir);
45
+ if (parent === dir || parent === parse(dir).root) {
46
+ return startDir;
47
+ }
48
+ dir = parent;
49
+ }
50
+ }
51
+
52
+ // src/shared/capabilities/environments-cache.ts
53
+ var CacheEntrySchema = external_exports.object({
54
+ path: external_exports.string(),
55
+ branch: external_exports.string(),
56
+ remote: external_exports.string().nullable(),
57
+ headMtimeMs: external_exports.number(),
58
+ cachedAt: external_exports.number()
59
+ });
60
+ var CacheFileSchema = external_exports.object({
61
+ entries: external_exports.record(external_exports.string(), CacheEntrySchema)
62
+ });
63
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
64
+ function cacheFilePath() {
65
+ return join2(getShipyardHome(), "data", "environments-cache.json");
66
+ }
67
+ async function loadCache() {
68
+ const filePath = cacheFilePath();
69
+ try {
70
+ const raw = await readFile(filePath, "utf8");
71
+ const parsed = JSON.parse(raw);
72
+ const result = CacheFileSchema.safeParse(parsed);
73
+ if (!result.success) {
74
+ logger.debug({ path: filePath }, "environments-cache: malformed, treating as empty");
75
+ return /* @__PURE__ */ new Map();
76
+ }
77
+ return new Map(Object.entries(result.data.entries));
78
+ } catch (err) {
79
+ if (isEnoent(err)) {
80
+ return /* @__PURE__ */ new Map();
81
+ }
82
+ logger.debug({ err }, "environments-cache: read failed, treating as empty");
83
+ return /* @__PURE__ */ new Map();
84
+ }
85
+ }
86
+ async function resolveHeadPath(worktreePath) {
87
+ const dotGitPath = join2(worktreePath, ".git");
88
+ try {
89
+ const dotGitStat = await stat(dotGitPath);
90
+ if (dotGitStat.isDirectory()) {
91
+ return join2(dotGitPath, "HEAD");
92
+ }
93
+ if (dotGitStat.isFile()) {
94
+ const contents = await readFile(dotGitPath, "utf8");
95
+ const match = contents.match(/^gitdir:\s*(.+)\s*$/m);
96
+ if (!match?.[1]) return null;
97
+ const gitdirRaw = match[1].trim();
98
+ const gitdir = isAbsolute(gitdirRaw) ? gitdirRaw : resolve(dirname2(dotGitPath), gitdirRaw);
99
+ return join2(gitdir, "HEAD");
100
+ }
101
+ return null;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+ async function getHeadMtime(worktreePath) {
107
+ const headPath = await resolveHeadPath(worktreePath);
108
+ if (!headPath) return null;
109
+ try {
110
+ const headStat = await stat(headPath);
111
+ return headStat.mtimeMs;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+ function isCacheValid(entry, currentMtimeMs) {
117
+ if (entry.headMtimeMs !== currentMtimeMs) return false;
118
+ if (Date.now() - entry.cachedAt >= CACHE_TTL_MS) return false;
119
+ return true;
120
+ }
121
+ async function saveCache(entries) {
122
+ const filePath = cacheFilePath();
123
+ const tmpPath = `${filePath}.tmp`;
124
+ try {
125
+ await mkdir(dirname2(filePath), { recursive: true });
126
+ const current = await loadCache();
127
+ for (const [path, entry] of entries) {
128
+ current.set(path, entry);
129
+ }
130
+ const payload = { entries: Object.fromEntries(current) };
131
+ await writeFile(tmpPath, JSON.stringify(payload), "utf8");
132
+ await rename(tmpPath, filePath);
133
+ } catch (err) {
134
+ logger.debug({ err, filePath }, "environments-cache: save failed");
135
+ void unlink(tmpPath).catch(() => {
136
+ });
137
+ }
138
+ }
139
+
140
+ // src/shared/capabilities/git-repo.ts
141
+ var gitRepoCache = /* @__PURE__ */ new Map();
142
+ async function isGitRepo(cwd) {
143
+ const cached = gitRepoCache.get(cwd);
144
+ if (cached !== void 0) return cached;
145
+ try {
146
+ await runWithTimeout("git", ["rev-parse", "--git-dir"], cwd, TIMEOUT_MS);
147
+ gitRepoCache.set(cwd, true);
148
+ return true;
149
+ } catch {
150
+ gitRepoCache.set(cwd, false);
151
+ return false;
152
+ }
153
+ }
154
+ var ghAvailableCache = false;
155
+ async function isGhAvailable() {
156
+ if (ghAvailableCache) return true;
157
+ try {
158
+ await run("which", ["gh"]);
159
+ ghAvailableCache = true;
160
+ return true;
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+ var topLevelCache = /* @__PURE__ */ new Map();
166
+ async function getGitTopLevel(cwd) {
167
+ const cached = topLevelCache.get(cwd);
168
+ if (cached !== void 0) return cached;
169
+ const topLevel = (await runWithTimeout("git", ["rev-parse", "--show-toplevel"], cwd, TIMEOUT_MS)).trim();
170
+ topLevelCache.set(cwd, topLevel);
171
+ return topLevel;
172
+ }
173
+ function parseOwnerRepo(remoteUrl) {
174
+ const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
175
+ if (!match?.[1] || !match[2]) return null;
176
+ return { owner: match[1], repo: match[2] };
177
+ }
178
+ async function getRepoDefaultBranch(cwd) {
179
+ try {
180
+ const head = (await runWithTimeout(
181
+ "git",
182
+ ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
183
+ cwd,
184
+ TIMEOUT_MS
185
+ )).trim();
186
+ if (!head) return null;
187
+ return head.replace(/^origin\//, "") || null;
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+ async function isAncestor(ancestor, descendant, cwd) {
193
+ try {
194
+ await runWithTimeout("git", ["merge-base", "--is-ancestor", ancestor, descendant], cwd, 5e3);
195
+ return true;
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+ var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
201
+ "node_modules",
202
+ "Library",
203
+ "Applications",
204
+ "Pictures",
205
+ "Music",
206
+ "Movies",
207
+ "go",
208
+ ".Trash"
209
+ ]);
210
+ var MAX_DEPTH = 4;
211
+ async function findGitRepos(dir, depth = 0) {
212
+ if (depth > MAX_DEPTH) return [];
213
+ try {
214
+ const entries = await readdir(dir, { withFileTypes: true });
215
+ for (const entry of entries) {
216
+ if (entry.name === ".git") {
217
+ return [dir];
218
+ }
219
+ }
220
+ if (depth >= MAX_DEPTH) return [];
221
+ const promises = [];
222
+ for (const entry of entries) {
223
+ if (!entry.isDirectory()) continue;
224
+ if (entry.name.startsWith(".")) continue;
225
+ if (EXCLUDE_DIRS.has(entry.name)) continue;
226
+ promises.push(findGitRepos(join3(dir, entry.name), depth + 1));
227
+ }
228
+ const results = await Promise.all(promises);
229
+ return results.flat();
230
+ } catch {
231
+ return [];
232
+ }
233
+ }
234
+ async function getRepoMetadata(repoPath, cached) {
235
+ try {
236
+ if (cached) {
237
+ return {
238
+ path: repoPath,
239
+ name: basename(repoPath),
240
+ branch: cached.branch,
241
+ ...cached.remote !== null && { remote: cached.remote }
242
+ };
243
+ }
244
+ const [branchResult, remoteResult] = await Promise.allSettled([
245
+ run("git", ["branch", "--show-current"], repoPath),
246
+ run("git", ["remote", "get-url", "origin"], repoPath)
247
+ ]);
248
+ const branch = branchResult.status === "fulfilled" ? branchResult.value || "HEAD" : "HEAD";
249
+ const remote = remoteResult.status === "fulfilled" ? remoteResult.value || void 0 : void 0;
250
+ return {
251
+ path: repoPath,
252
+ name: basename(repoPath),
253
+ branch,
254
+ ...remote && { remote }
255
+ };
256
+ } catch {
257
+ return null;
258
+ }
259
+ }
260
+ async function detectEnvironments() {
261
+ const { homedir } = await import("os");
262
+ const repoPaths = await findGitRepos(homedir());
263
+ const worktreePaths = await discoverWorktrees(repoPaths);
264
+ const allPaths = [.../* @__PURE__ */ new Set([...repoPaths, ...worktreePaths])];
265
+ const prevCache = await loadCache();
266
+ const headMtimes = await Promise.all(allPaths.map(getHeadMtime));
267
+ const nextCache = /* @__PURE__ */ new Map();
268
+ const resolved = await Promise.all(
269
+ allPaths.map(async (path, idx) => {
270
+ const currentMtime = headMtimes[idx] ?? null;
271
+ const prev = prevCache.get(path);
272
+ const cacheHit = prev !== void 0 && currentMtime !== null && isCacheValid(prev, currentMtime);
273
+ const info = await getRepoMetadata(path, cacheHit ? prev : void 0);
274
+ if (info !== null && currentMtime !== null) {
275
+ nextCache.set(path, {
276
+ path,
277
+ branch: info.branch,
278
+ remote: info.remote ?? null,
279
+ headMtimeMs: currentMtime,
280
+ cachedAt: cacheHit && prev ? prev.cachedAt : Date.now()
281
+ });
282
+ }
283
+ return info;
284
+ })
285
+ );
286
+ await saveCache(nextCache);
287
+ return resolved.filter((info) => info !== null);
288
+ }
289
+ async function discoverWorktrees(repoPaths) {
290
+ const results = await Promise.allSettled(repoPaths.map(listWorktreePaths));
291
+ return results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
292
+ }
293
+ async function listWorktreePaths(repoPath) {
294
+ try {
295
+ const output = await runWithTimeout(
296
+ "git",
297
+ ["worktree", "list", "--porcelain"],
298
+ repoPath,
299
+ TIMEOUT_MS
300
+ );
301
+ return parseWorktreeListOutput(output, repoPath);
302
+ } catch {
303
+ return [];
304
+ }
305
+ }
306
+ function parseWorktreeListOutput(output, repoPath) {
307
+ const paths = [];
308
+ for (const line of output.split("\n")) {
309
+ if (line.startsWith("worktree ")) {
310
+ const wt = line.slice("worktree ".length).trim();
311
+ if (wt && wt !== repoPath) paths.push(wt);
312
+ }
313
+ }
314
+ return paths;
315
+ }
316
+ var _testing = {
317
+ parseWorktreeListOutput,
318
+ resetGhAvailableCache: () => {
319
+ ghAvailableCache = false;
320
+ }
321
+ };
322
+
323
+ export {
324
+ isEnoent,
325
+ isEexist,
326
+ isCorruptionError,
327
+ findProjectRoot,
328
+ gitRepoCache,
329
+ isGitRepo,
330
+ isGhAvailable,
331
+ getGitTopLevel,
332
+ parseOwnerRepo,
333
+ getRepoDefaultBranch,
334
+ isAncestor,
335
+ findGitRepos,
336
+ getRepoMetadata,
337
+ detectEnvironments,
338
+ _testing
339
+ };
340
+ //# sourceMappingURL=chunk-G5KJXNNK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/capabilities/git-repo.ts","../src/shared/capabilities/environments-cache.ts","../src/shared/fs-utils.ts"],"sourcesContent":["import { readdir } from 'node:fs/promises';\nimport { basename, join } from 'node:path';\nimport type { GitRepoInfo } from '@shipyard/session';\nimport {\n type CacheEntry,\n getHeadMtime,\n isCacheValid,\n loadCache,\n saveCache,\n} from './environments-cache.js';\nimport { run, runWithTimeout, TIMEOUT_MS } from './shell.js';\n\n/** Cache of directory paths to whether they are inside a git repository. */\nexport const gitRepoCache = new Map<string, boolean>();\n\n/**\n * Check whether the given directory is inside a git work-tree.\n * Results are cached per directory so repeated calls (e.g. during\n * debounced diff captures) do not re-spawn git processes.\n */\nexport async function isGitRepo(cwd: string): Promise<boolean> {\n const cached = gitRepoCache.get(cwd);\n if (cached !== undefined) return cached;\n try {\n await runWithTimeout('git', ['rev-parse', '--git-dir'], cwd, TIMEOUT_MS);\n gitRepoCache.set(cwd, true);\n return true;\n } catch {\n gitRepoCache.set(cwd, false);\n return false;\n }\n}\n\nlet ghAvailableCache = false;\n\nexport async function isGhAvailable(): Promise<boolean> {\n if (ghAvailableCache) return true;\n try {\n await run('which', ['gh']);\n ghAvailableCache = true;\n return true;\n } catch {\n return false;\n }\n}\n\nconst topLevelCache = new Map<string, string>();\n\nexport async function getGitTopLevel(cwd: string): Promise<string> {\n const cached = topLevelCache.get(cwd);\n if (cached !== undefined) return cached;\n const topLevel = (\n await runWithTimeout('git', ['rev-parse', '--show-toplevel'], cwd, TIMEOUT_MS)\n ).trim();\n topLevelCache.set(cwd, topLevel);\n return topLevel;\n}\n\nexport function parseOwnerRepo(remoteUrl: string): { owner: string; repo: string } | null {\n const match = remoteUrl.match(/github\\.com[:/]([^/]+)\\/([^/.]+)/);\n if (!match?.[1] || !match[2]) return null;\n return { owner: match[1], repo: match[2] };\n}\n\n/**\n * Look up the repo's default branch via the `origin/HEAD` symbolic ref.\n * Returns the branch name (e.g. `main`) or null when the symbolic ref\n * isn't set (rare — happens for repos cloned without `git remote\n * set-head` or local-only repos).\n */\nexport async function getRepoDefaultBranch(cwd: string): Promise<string | null> {\n try {\n const head = (\n await runWithTimeout(\n 'git',\n ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'],\n cwd,\n TIMEOUT_MS\n )\n ).trim();\n if (!head) return null;\n return head.replace(/^origin\\//, '') || null;\n } catch {\n return null;\n }\n}\n\nexport async function isAncestor(\n ancestor: string,\n descendant: string,\n cwd: string\n): Promise<boolean> {\n try {\n await runWithTimeout('git', ['merge-base', '--is-ancestor', ancestor, descendant], cwd, 5_000);\n return true;\n } catch {\n return false;\n }\n}\n\nconst EXCLUDE_DIRS = new Set([\n 'node_modules',\n 'Library',\n 'Applications',\n 'Pictures',\n 'Music',\n 'Movies',\n 'go',\n '.Trash',\n]);\n\nconst MAX_DEPTH = 4;\n\nexport async function findGitRepos(dir: string, depth = 0): Promise<string[]> {\n if (depth > MAX_DEPTH) return [];\n\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.name === '.git') {\n return [dir];\n }\n }\n\n if (depth >= MAX_DEPTH) return [];\n\n const promises: Promise<string[]>[] = [];\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (entry.name.startsWith('.')) continue;\n if (EXCLUDE_DIRS.has(entry.name)) continue;\n promises.push(findGitRepos(join(dir, entry.name), depth + 1));\n }\n\n const results = await Promise.all(promises);\n return results.flat();\n } catch {\n return [];\n }\n}\n\nexport async function getRepoMetadata(\n repoPath: string,\n cached?: CacheEntry\n): Promise<GitRepoInfo | null> {\n try {\n if (cached) {\n return {\n path: repoPath,\n name: basename(repoPath),\n branch: cached.branch,\n ...(cached.remote !== null && { remote: cached.remote }),\n };\n }\n\n const [branchResult, remoteResult] = await Promise.allSettled([\n run('git', ['branch', '--show-current'], repoPath),\n run('git', ['remote', 'get-url', 'origin'], repoPath),\n ]);\n\n const branch = branchResult.status === 'fulfilled' ? branchResult.value || 'HEAD' : 'HEAD';\n const remote =\n remoteResult.status === 'fulfilled' ? remoteResult.value || undefined : undefined;\n\n return {\n path: repoPath,\n name: basename(repoPath),\n branch,\n ...(remote && { remote }),\n };\n } catch {\n return null;\n }\n}\n\nexport async function detectEnvironments(): Promise<GitRepoInfo[]> {\n const { homedir } = await import('node:os');\n const repoPaths = await findGitRepos(homedir());\n const worktreePaths = await discoverWorktrees(repoPaths);\n const allPaths = [...new Set([...repoPaths, ...worktreePaths])];\n\n const prevCache = await loadCache();\n const headMtimes = await Promise.all(allPaths.map(getHeadMtime));\n const nextCache = new Map<string, CacheEntry>();\n\n const resolved = await Promise.all(\n allPaths.map(async (path, idx) => {\n const currentMtime = headMtimes[idx] ?? null;\n const prev = prevCache.get(path);\n const cacheHit =\n prev !== undefined && currentMtime !== null && isCacheValid(prev, currentMtime);\n const info = await getRepoMetadata(path, cacheHit ? prev : undefined);\n if (info !== null && currentMtime !== null) {\n nextCache.set(path, {\n path,\n branch: info.branch,\n remote: info.remote ?? null,\n headMtimeMs: currentMtime,\n cachedAt: cacheHit && prev ? prev.cachedAt : Date.now(),\n });\n }\n return info;\n })\n );\n\n await saveCache(nextCache);\n\n return resolved.filter((info): info is GitRepoInfo => info !== null);\n}\n\n/**\n * For each repo, run `git worktree list --porcelain` to discover\n * linked worktrees (which may live in dot-directories like .claude/worktrees/).\n */\nasync function discoverWorktrees(repoPaths: string[]): Promise<string[]> {\n const results = await Promise.allSettled(repoPaths.map(listWorktreePaths));\n return results.flatMap((r) => (r.status === 'fulfilled' ? r.value : []));\n}\n\nasync function listWorktreePaths(repoPath: string): Promise<string[]> {\n try {\n const output = await runWithTimeout(\n 'git',\n ['worktree', 'list', '--porcelain'],\n repoPath,\n TIMEOUT_MS\n );\n return parseWorktreeListOutput(output, repoPath);\n } catch {\n return [];\n }\n}\n\nfunction parseWorktreeListOutput(output: string, repoPath: string): string[] {\n const paths: string[] = [];\n for (const line of output.split('\\n')) {\n if (line.startsWith('worktree ')) {\n const wt = line.slice('worktree '.length).trim();\n if (wt && wt !== repoPath) paths.push(wt);\n }\n }\n return paths;\n}\n\nexport const _testing = {\n parseWorktreeListOutput,\n resetGhAvailableCache: () => {\n ghAvailableCache = false;\n },\n};\n","import { mkdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, resolve } from 'node:path';\nimport { z } from 'zod';\nimport { getShipyardHome } from '../env.js';\nimport { isEnoent } from '../fs-utils.js';\nimport { logger } from '../logger.js';\n\nconst CacheEntrySchema = z.object({\n path: z.string(),\n branch: z.string(),\n remote: z.string().nullable(),\n headMtimeMs: z.number(),\n cachedAt: z.number(),\n});\n\nconst CacheFileSchema = z.object({\n entries: z.record(z.string(), CacheEntrySchema),\n});\n\nexport type CacheEntry = z.infer<typeof CacheEntrySchema>;\ntype CacheFile = z.infer<typeof CacheFileSchema>;\n\nexport const CACHE_TTL_MS = 24 * 60 * 60 * 1000;\n\nfunction cacheFilePath(): string {\n return join(getShipyardHome(), 'data', 'environments-cache.json');\n}\n\nexport async function loadCache(): Promise<Map<string, CacheEntry>> {\n const filePath = cacheFilePath();\n try {\n const raw = await readFile(filePath, 'utf8');\n const parsed: unknown = JSON.parse(raw);\n const result = CacheFileSchema.safeParse(parsed);\n if (!result.success) {\n logger.debug({ path: filePath }, 'environments-cache: malformed, treating as empty');\n return new Map();\n }\n return new Map(Object.entries(result.data.entries));\n } catch (err) {\n if (isEnoent(err)) {\n return new Map();\n }\n logger.debug({ err }, 'environments-cache: read failed, treating as empty');\n return new Map();\n }\n}\n\n/**\n * Resolve the actual path of HEAD for the given worktree.\n *\n * - If `<path>/.git` is a directory, HEAD lives at `<path>/.git/HEAD`.\n * - If `<path>/.git` is a file (linked worktree), it contains `gitdir: <path>`;\n * HEAD lives at `<gitdir>/HEAD`. The gitdir may be relative to `<path>`.\n * - Returns null on any error (missing, unreadable, malformed).\n */\nexport async function resolveHeadPath(worktreePath: string): Promise<string | null> {\n const dotGitPath = join(worktreePath, '.git');\n try {\n const dotGitStat = await stat(dotGitPath);\n if (dotGitStat.isDirectory()) {\n return join(dotGitPath, 'HEAD');\n }\n if (dotGitStat.isFile()) {\n const contents = await readFile(dotGitPath, 'utf8');\n const match = contents.match(/^gitdir:\\s*(.+)\\s*$/m);\n if (!match?.[1]) return null;\n const gitdirRaw = match[1].trim();\n const gitdir = isAbsolute(gitdirRaw) ? gitdirRaw : resolve(dirname(dotGitPath), gitdirRaw);\n return join(gitdir, 'HEAD');\n }\n return null;\n } catch {\n return null;\n }\n}\n\nexport async function getHeadMtime(worktreePath: string): Promise<number | null> {\n const headPath = await resolveHeadPath(worktreePath);\n if (!headPath) return null;\n try {\n const headStat = await stat(headPath);\n return headStat.mtimeMs;\n } catch {\n return null;\n }\n}\n\nexport function isCacheValid(entry: CacheEntry, currentMtimeMs: number): boolean {\n if (entry.headMtimeMs !== currentMtimeMs) return false;\n if (Date.now() - entry.cachedAt >= CACHE_TTL_MS) return false;\n return true;\n}\n\nexport async function saveCache(entries: Map<string, CacheEntry>): Promise<void> {\n const filePath = cacheFilePath();\n const tmpPath = `${filePath}.tmp`;\n try {\n await mkdir(dirname(filePath), { recursive: true });\n /**\n * Merge-on-save: re-read disk immediately before writing so a concurrent\n * saveCache call's entries are preserved rather than erased (new entries\n * win for paths present in both — they are fresher).\n */\n const current = await loadCache();\n for (const [path, entry] of entries) {\n current.set(path, entry);\n }\n const payload: CacheFile = { entries: Object.fromEntries(current) };\n await writeFile(tmpPath, JSON.stringify(payload), 'utf8');\n await rename(tmpPath, filePath);\n } catch (err) {\n logger.debug({ err, filePath }, 'environments-cache: save failed');\n void unlink(tmpPath).catch(() => {});\n }\n}\n","import { existsSync } from 'node:fs';\nimport { dirname, join, parse } from 'node:path';\n\nexport function isEnoent(err: unknown): boolean {\n return typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT';\n}\n\nexport function isEexist(err: unknown): boolean {\n return typeof err === 'object' && err !== null && 'code' in err && err.code === 'EEXIST';\n}\n\n/**\n * Detect JSON parse errors or Zod validation failures that indicate\n * a corrupt store file. Shared across all file-backed stores.\n */\nexport function isCorruptionError(err: unknown): boolean {\n if (err instanceof SyntaxError) return true;\n if (typeof err === 'object' && err !== null && 'name' in err && err.name === 'ZodError')\n return true;\n return false;\n}\n\n/**\n * Walk up the directory tree from `startDir` to find the project root.\n *\n * Looks for `.git` (directory or file — worktrees use a `.git` file) or\n * `pnpm-workspace.yaml` as root markers. Falls back to `startDir` itself\n * when no marker is found (e.g. running outside a repo).\n *\n * In dev: `startDir` is typically `apps/daemon/` (pnpm runs from package dir),\n * so this walks up to find the monorepo root. In prod install: `startDir` is\n * the user's project directory where the daemon is launched, and `.git` is\n * found immediately. Called once at daemon startup — the result is used as\n * the workspace root for file-io, LSP, and file watching.\n */\nexport function findProjectRoot(startDir: string): string {\n let dir = startDir;\n while (true) {\n if (existsSync(join(dir, 'pnpm-workspace.yaml')) || existsSync(join(dir, '.git'))) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir || parent === parse(dir).root) {\n return startDir;\n }\n dir = parent;\n }\n}\n"],"mappings":"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","names":["join","dirname","join","join","dirname","join"]}
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/shared/wasm-panic-buffer.ts
4
+ var BUFFER_SIZE = 8;
5
+ var PANIC_MARKERS = ["panicked at", "unreachable executed", "called `Option::unwrap()`"];
6
+ var buffer = [];
7
+ var installed = false;
8
+ var originalConsoleError = null;
9
+ function installWasmPanicBuffer() {
10
+ if (installed) return;
11
+ installed = true;
12
+ originalConsoleError = console.error.bind(console);
13
+ console.error = (...args) => {
14
+ const text = args.map(
15
+ (arg) => arg instanceof Error ? `${arg.message}
16
+ ${arg.stack ?? ""}` : typeof arg === "string" ? arg : safeStringify(arg)
17
+ ).join(" ");
18
+ if (PANIC_MARKERS.some((marker) => text.includes(marker))) {
19
+ buffer.push({ capturedAtMs: Date.now(), text });
20
+ while (buffer.length > BUFFER_SIZE) buffer.shift();
21
+ }
22
+ originalConsoleError?.(...args);
23
+ };
24
+ }
25
+ function getRecentWasmPanicMessages() {
26
+ return buffer;
27
+ }
28
+ function safeStringify(value) {
29
+ try {
30
+ return JSON.stringify(value);
31
+ } catch {
32
+ return String(value);
33
+ }
34
+ }
35
+
36
+ export {
37
+ installWasmPanicBuffer,
38
+ getRecentWasmPanicMessages
39
+ };
40
+ //# sourceMappingURL=chunk-GQPRMFVC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/wasm-panic-buffer.ts"],"sourcesContent":["export { installWasmPanicBuffer, getRecentWasmPanicMessages, resetWasmPanicBufferForTest };\n\n/**\n * Ring buffer of the last N `console.error` messages that match the\n * `console_error_panic_hook` output shape. The hook is installed on the\n * Rust side at `crates/loro-wasm/src/lib.rs:64-67` (already shipped in the\n * vendored Loro fork at `1.11.1-shipyard-main-7dfda879.0`) — it captures\n * the panic location (file:line) + payload and writes it to `console.error`\n * BEFORE the wasm trap fires.\n *\n * Without this Node-side capture, that message hits the daemon's stderr\n * stream and is gone — the `process.on('uncaughtException')` handler in\n * `lifecycle.ts:#handleWasmPanic` only sees the JS exception (`RuntimeError:\n * unreachable`), not the Rust panic site that fired a moment earlier.\n *\n * With this capture in place, `#handleWasmPanic` can pull the most recent\n * Rust panic message and include it in the structured log, finally\n * resolving the ambiguity from Invariant #14: bare `unreachable` is no\n * longer indistinguishable between lock-order, state-machine, and\n * pending-changes panics — the actual Rust file:line is recorded.\n *\n * MUST be installed before any `loro-crdt` import resolves (i.e. at daemon\n * entry, before the dynamic import of `serve.js`). Calling it after the\n * wasm module loads is a no-op for already-emitted panics — only future\n * panics will be captured.\n */\n\nconst BUFFER_SIZE = 8;\nconst PANIC_MARKERS = ['panicked at', 'unreachable executed', 'called `Option::unwrap()`'];\n\ninterface BufferedMessage {\n capturedAtMs: number;\n text: string;\n}\n\nconst buffer: BufferedMessage[] = [];\nlet installed = false;\nlet originalConsoleError: typeof console.error | null = null;\n\nfunction installWasmPanicBuffer(): void {\n if (installed) return;\n installed = true;\n // biome-ignore lint/suspicious/noConsole: this file's entire purpose is to intercept console.error so the Rust panic hook output (which writes to console.error before the wasm trap fires) can be captured.\n originalConsoleError = console.error.bind(console);\n\n console.error = (...args: unknown[]): void => {\n /**\n * Coalesce all arguments into a single string. wasm-bindgen typically\n * calls `console.error(panicMessage)` with one arg, but the Rust panic\n * hook's exact format is implementation-defined and may include\n * trailing newlines or multiple arguments. Stringifying the lot makes\n * marker matching robust to format drift.\n */\n const text = args\n .map((arg) =>\n arg instanceof Error\n ? `${arg.message}\\n${arg.stack ?? ''}`\n : typeof arg === 'string'\n ? arg\n : safeStringify(arg)\n )\n .join(' ');\n\n if (PANIC_MARKERS.some((marker) => text.includes(marker))) {\n buffer.push({ capturedAtMs: Date.now(), text });\n while (buffer.length > BUFFER_SIZE) buffer.shift();\n }\n\n originalConsoleError?.(...args);\n };\n}\n\n/**\n * Return the buffered Rust panic messages. Newest last. Caller does not\n * mutate the result; the buffer continues to grow in place.\n */\nfunction getRecentWasmPanicMessages(): readonly BufferedMessage[] {\n return buffer;\n}\n\n/**\n * Test-only reset hook. Drains the buffer and uninstalls the interceptor\n * so the next test setup gets a clean slate.\n */\nfunction resetWasmPanicBufferForTest(): void {\n buffer.length = 0;\n if (installed && originalConsoleError) {\n console.error = originalConsoleError;\n }\n installed = false;\n originalConsoleError = null;\n}\n\nfunction safeStringify(value: unknown): string {\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n"],"mappings":";;;AA2BA,IAAM,cAAc;AACpB,IAAM,gBAAgB,CAAC,eAAe,wBAAwB,2BAA2B;AAOzF,IAAM,SAA4B,CAAC;AACnC,IAAI,YAAY;AAChB,IAAI,uBAAoD;AAExD,SAAS,yBAA+B;AACtC,MAAI,UAAW;AACf,cAAY;AAEZ,yBAAuB,QAAQ,MAAM,KAAK,OAAO;AAEjD,UAAQ,QAAQ,IAAI,SAA0B;AAQ5C,UAAM,OAAO,KACV;AAAA,MAAI,CAAC,QACJ,eAAe,QACX,GAAG,IAAI,OAAO;AAAA,EAAK,IAAI,SAAS,EAAE,KAClC,OAAO,QAAQ,WACb,MACA,cAAc,GAAG;AAAA,IACzB,EACC,KAAK,GAAG;AAEX,QAAI,cAAc,KAAK,CAAC,WAAW,KAAK,SAAS,MAAM,CAAC,GAAG;AACzD,aAAO,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,KAAK,CAAC;AAC9C,aAAO,OAAO,SAAS,YAAa,QAAO,MAAM;AAAA,IACnD;AAEA,2BAAuB,GAAG,IAAI;AAAA,EAChC;AACF;AAMA,SAAS,6BAAyD;AAChE,SAAO;AACT;AAeA,SAAS,cAAc,OAAwB;AAC7C,MAAI;AACF,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B,QAAQ;AACN,WAAO,OAAO,KAAK;AAAA,EACrB;AACF;","names":[]}
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/shared/capabilities/shell.ts
4
+ import { execFile } from "child_process";
5
+ var TIMEOUT_MS = 5e3;
6
+ var AUTH_STATUS_TIMEOUT_MS = 15e3;
7
+ function run(command, args, cwd, timeoutMs = TIMEOUT_MS) {
8
+ return new Promise((resolve, reject) => {
9
+ execFile(command, args, { timeout: timeoutMs, cwd }, (error, stdout) => {
10
+ if (error) {
11
+ reject(error);
12
+ return;
13
+ }
14
+ resolve(stdout.trim());
15
+ });
16
+ });
17
+ }
18
+ function runWithTimeout(command, args, cwd, timeoutMs) {
19
+ return new Promise((resolve, reject) => {
20
+ execFile(
21
+ command,
22
+ args,
23
+ { timeout: timeoutMs, cwd, maxBuffer: 10 * 1024 * 1024 },
24
+ (error, stdout) => {
25
+ if (error) {
26
+ reject(error);
27
+ return;
28
+ }
29
+ resolve(stdout.trim());
30
+ }
31
+ );
32
+ });
33
+ }
34
+ var DIFF_TIMEOUT_MS = 15e3;
35
+ var MAX_DIFF_SIZE = 5e5;
36
+ function isMaxBufferError(err) {
37
+ return err instanceof Error && err.message.includes("maxBuffer");
38
+ }
39
+ var BUFFER_OVERFLOW_MSG = "... diff too large (exceeded buffer limit) ...\n";
40
+ function truncateDiff(result) {
41
+ if (result.length <= MAX_DIFF_SIZE) return result;
42
+ const boundary = result.lastIndexOf("\ndiff --git ", MAX_DIFF_SIZE);
43
+ const cutoff = boundary > 0 ? boundary : MAX_DIFF_SIZE;
44
+ return `${result.slice(0, cutoff)}
45
+
46
+ ... diff truncated (exceeds 500KB) ...
47
+ `;
48
+ }
49
+
50
+ export {
51
+ TIMEOUT_MS,
52
+ AUTH_STATUS_TIMEOUT_MS,
53
+ run,
54
+ runWithTimeout,
55
+ DIFF_TIMEOUT_MS,
56
+ MAX_DIFF_SIZE,
57
+ isMaxBufferError,
58
+ BUFFER_OVERFLOW_MSG,
59
+ truncateDiff
60
+ };
61
+ //# sourceMappingURL=chunk-KYLY4DJF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/capabilities/shell.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\n\nexport const TIMEOUT_MS = 5_000;\nexport const AUTH_STATUS_TIMEOUT_MS = 15_000;\n\nexport function run(\n command: string,\n args: string[],\n cwd?: string,\n timeoutMs: number = TIMEOUT_MS\n): Promise<string> {\n return new Promise((resolve, reject) => {\n execFile(command, args, { timeout: timeoutMs, cwd }, (error, stdout) => {\n if (error) {\n reject(error);\n return;\n }\n resolve(stdout.trim());\n });\n });\n}\n\nexport function runWithTimeout(\n command: string,\n args: string[],\n cwd: string,\n timeoutMs: number\n): Promise<string> {\n return new Promise((resolve, reject) => {\n execFile(\n command,\n args,\n { timeout: timeoutMs, cwd, maxBuffer: 10 * 1024 * 1024 },\n (error, stdout) => {\n if (error) {\n reject(error);\n return;\n }\n resolve(stdout.trim());\n }\n );\n });\n}\n\nexport const DIFF_TIMEOUT_MS = 15_000;\nexport const MAX_DIFF_SIZE = 500_000;\n\nexport function isMaxBufferError(err: unknown): boolean {\n return err instanceof Error && err.message.includes('maxBuffer');\n}\n\nexport const BUFFER_OVERFLOW_MSG = '... diff too large (exceeded buffer limit) ...\\n';\n\nexport function truncateDiff(result: string): string {\n if (result.length <= MAX_DIFF_SIZE) return result;\n const boundary = result.lastIndexOf('\\ndiff --git ', MAX_DIFF_SIZE);\n const cutoff = boundary > 0 ? boundary : MAX_DIFF_SIZE;\n return `${result.slice(0, cutoff)}\\n\\n... diff truncated (exceeds 500KB) ...\\n`;\n}\n"],"mappings":";;;AAAA,SAAS,gBAAgB;AAElB,IAAM,aAAa;AACnB,IAAM,yBAAyB;AAE/B,SAAS,IACd,SACA,MACA,KACA,YAAoB,YACH;AACjB,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,aAAS,SAAS,MAAM,EAAE,SAAS,WAAW,IAAI,GAAG,CAAC,OAAO,WAAW;AACtE,UAAI,OAAO;AACT,eAAO,KAAK;AACZ;AAAA,MACF;AACA,cAAQ,OAAO,KAAK,CAAC;AAAA,IACvB,CAAC;AAAA,EACH,CAAC;AACH;AAEO,SAAS,eACd,SACA,MACA,KACA,WACiB;AACjB,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC;AAAA,MACE;AAAA,MACA;AAAA,MACA,EAAE,SAAS,WAAW,KAAK,WAAW,KAAK,OAAO,KAAK;AAAA,MACvD,CAAC,OAAO,WAAW;AACjB,YAAI,OAAO;AACT,iBAAO,KAAK;AACZ;AAAA,QACF;AACA,gBAAQ,OAAO,KAAK,CAAC;AAAA,MACvB;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEO,IAAM,kBAAkB;AACxB,IAAM,gBAAgB;AAEtB,SAAS,iBAAiB,KAAuB;AACtD,SAAO,eAAe,SAAS,IAAI,QAAQ,SAAS,WAAW;AACjE;AAEO,IAAM,sBAAsB;AAE5B,SAAS,aAAa,QAAwB;AACnD,MAAI,OAAO,UAAU,cAAe,QAAO;AAC3C,QAAM,WAAW,OAAO,YAAY,iBAAiB,aAAa;AAClE,QAAM,SAAS,WAAW,IAAI,WAAW;AACzC,SAAO,GAAG,OAAO,MAAM,GAAG,MAAM,CAAC;AAAA;AAAA;AAAA;AACnC;","names":[]}
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ _testing,
4
+ detectEnvironments,
5
+ findGitRepos,
6
+ getGitTopLevel,
7
+ getRepoDefaultBranch,
8
+ getRepoMetadata,
9
+ gitRepoCache,
10
+ isAncestor,
11
+ isGhAvailable,
12
+ isGitRepo,
13
+ parseOwnerRepo
14
+ } from "./chunk-G5KJXNNK.js";
15
+ import "./chunk-KYLY4DJF.js";
16
+ import "./chunk-DNIC3FOH.js";
17
+ import "./chunk-M5M6VC5F.js";
18
+ import "./chunk-2H7UOFLK.js";
19
+ export {
20
+ _testing,
21
+ detectEnvironments,
22
+ findGitRepos,
23
+ getGitTopLevel,
24
+ getRepoDefaultBranch,
25
+ getRepoMetadata,
26
+ gitRepoCache,
27
+ isAncestor,
28
+ isGhAvailable,
29
+ isGitRepo,
30
+ parseOwnerRepo
31
+ };
32
+ //# sourceMappingURL=git-repo-I2OABFB7.js.map
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ installWasmPanicBuffer
4
+ } from "./chunk-GQPRMFVC.js";
2
5
  import {
3
6
  logger
4
7
  } from "./chunk-DNIC3FOH.js";
@@ -12,6 +15,8 @@ import { readFileSync } from "fs";
12
15
  import { resolve } from "path";
13
16
  import { fileURLToPath } from "url";
14
17
  import { parseArgs } from "util";
18
+ Error.stackTraceLimit = 50;
19
+ installWasmPanicBuffer();
15
20
  function getVersion() {
16
21
  try {
17
22
  const thisFile = fileURLToPath(import.meta.url);
@@ -95,7 +100,7 @@ async function loadAuthFromConfig(env) {
95
100
  async function handleSubcommand() {
96
101
  const subcommand = process.argv[2];
97
102
  if (subcommand === "login") {
98
- const { loginCommand } = await import("./login-N43V35KE.js");
103
+ const { loginCommand } = await import("./login-D3OUX3KD.js");
99
104
  const hasCheck = process.argv.includes("--check");
100
105
  await loginCommand({ check: hasCheck });
101
106
  return true;
@@ -106,7 +111,7 @@ async function handleSubcommand() {
106
111
  return true;
107
112
  }
108
113
  if (subcommand === "start") {
109
- const { startCommand } = await import("./start-NVYMND3X.js");
114
+ const { startCommand } = await import("./start-ZOGQS3QS.js");
110
115
  await startCommand();
111
116
  return true;
112
117
  }
@@ -123,7 +128,7 @@ async function main() {
123
128
  const args = parseCliArgs();
124
129
  if (args.serve) {
125
130
  await loadAuthFromConfig(env);
126
- const { serve } = await import("./serve-QLYUK5XN.js");
131
+ const { serve } = await import("./serve-MBUBBBRA.js");
127
132
  return serve({ isDev: env.SHIPYARD_DEV });
128
133
  }
129
134
  logger.error("Use `shipyard start` to run the daemon. Use --help for usage.");
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { parseArgs } from 'node:util';\nimport { type Env, validateEnv } from './shared/env.js';\nimport { logger } from './shared/logger.js';\n\nfunction getVersion(): string {\n try {\n const thisFile = fileURLToPath(import.meta.url);\n const pkgPath = resolve(thisFile, '../../package.json');\n const pkg: { version?: string } = JSON.parse(readFileSync(pkgPath, 'utf-8'));\n return pkg.version ?? 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\ninterface CliArgs {\n serve?: boolean;\n}\n\nfunction parseCliArgs(): CliArgs {\n const { values } = parseArgs({\n options: {\n serve: { type: 'boolean', short: 's' },\n version: { type: 'boolean', short: 'v' },\n help: { type: 'boolean', short: 'h' },\n },\n strict: true,\n });\n\n if (values.version) {\n process.stdout.write(`${getVersion()}\\n`);\n process.exit(0);\n }\n\n if (values.help) {\n logger.info(\n [\n `shipyard v${getVersion()} - Ship together with AI — locally, visibly, confidently`,\n '',\n 'Usage:',\n ' shipyard start [--code CODE] Start daemon (authenticates if needed, opens browser)',\n ' shipyard login Authenticate with Shipyard',\n ' shipyard login --check Check current auth status',\n ' shipyard logout Clear stored credentials',\n '',\n 'Options:',\n ' -v, --version Show version',\n ' -h, --help Show this help',\n '',\n 'Authentication:',\n ' Run `claude auth login` to authenticate with Anthropic (primary method).',\n ' Alternatively, set ANTHROPIC_API_KEY for CI/headless environments.',\n '',\n 'Environment:',\n ' ANTHROPIC_API_KEY API key for Claude (optional, overrides OAuth)',\n ' SHIPYARD_DEV Set to 1 for dev mode (uses ~/.shipyard-dev/)',\n ' SHIPYARD_WEB_URL Override browser URL for auto-open',\n ' LOG_LEVEL Log level: debug, info, warn, error (default: info)',\n ' SHIPYARD_SIGNALING_URL Signaling server WebSocket URL (optional)',\n ' SHIPYARD_USER_TOKEN JWT for signaling auth (optional)',\n ' SHIPYARD_USER_ID User ID for signaling path (optional, from login)',\n ' SHIPYARD_MACHINE_ID Machine identifier (default: os.hostname())',\n ' SHIPYARD_MACHINE_NAME Human-readable machine name (default: os.hostname())',\n ].join('\\n')\n );\n process.exit(0);\n }\n\n return {\n serve: values.serve,\n };\n}\n\nasync function loadAuthFromConfig(env: Env): Promise<void> {\n if (env.SHIPYARD_USER_TOKEN) return;\n\n const { loadAuthToken } = await import('./services/bootstrap/auth.js');\n const auth = await loadAuthToken();\n\n if (auth.status === 'ok') {\n env.SHIPYARD_USER_TOKEN = auth.token;\n env.SHIPYARD_USER_ID = auth.userId;\n env.SHIPYARD_USER_DISPLAY_NAME = auth.displayName;\n if (auth.signalingUrl) {\n env.SHIPYARD_SIGNALING_URL = auth.signalingUrl;\n }\n return;\n }\n\n if (auth.status === 'expired') {\n logger.warn('Auth token expired. Run `shipyard login` to re-authenticate.');\n return;\n }\n\n logger.warn('No auth token found. Run `shipyard login` to authenticate.');\n}\n\nasync function handleSubcommand(): Promise<boolean> {\n const subcommand = process.argv[2];\n\n if (subcommand === 'login') {\n const { loginCommand } = await import('./shared/commands/login.js');\n const hasCheck = process.argv.includes('--check');\n await loginCommand({ check: hasCheck });\n return true;\n }\n\n if (subcommand === 'logout') {\n const { logoutCommand } = await import('./shared/commands/logout.js');\n await logoutCommand();\n return true;\n }\n\n if (subcommand === 'start') {\n const { startCommand } = await import('./shared/commands/start.js');\n await startCommand();\n return true;\n }\n\n if (subcommand === 'roi') {\n const { roiCommand } = await import('./shared/commands/roi.js');\n await roiCommand();\n return true;\n }\n\n return false;\n}\n\nasync function main(): Promise<void> {\n if (await handleSubcommand()) return;\n\n const env = validateEnv();\n const args = parseCliArgs();\n\n if (args.serve) {\n await loadAuthFromConfig(env);\n const { serve } = await import('./services/serve.js');\n return serve({ isDev: env.SHIPYARD_DEV });\n }\n\n logger.error('Use `shipyard start` to run the daemon. Use --help for usage.');\n process.exit(1);\n}\n\nmain().catch((error: unknown) => {\n const errMsg = error instanceof Error ? error.message : String(error);\n const errStack = error instanceof Error ? error.stack : undefined;\n logger.error({ err: errMsg, stack: errStack }, 'Fatal error');\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { parseArgs } from 'node:util';\nimport { type Env, validateEnv } from './shared/env.js';\nimport { logger } from './shared/logger.js';\nimport { installWasmPanicBuffer } from './shared/wasm-panic-buffer.js';\n\n/**\n * Node defaults to `Error.stackTraceLimit = 10`. Loro wasm panics produce\n * exactly 10-frame wasm chains, which means any JS frames *above* the wasm\n * boundary are silently truncated and never reach `panic-watch.log`.\n * Raising this lets the JS-stack capture (issue #2354) actually see\n * caller-side frames so we can distinguish JS-triggered re-entry from\n * pure-wasm cascade poison.\n */\nError.stackTraceLimit = 50;\n\n/**\n * Install the `console.error` ring buffer BEFORE any `loro-crdt` import\n * resolves. The vendored Loro fork has `console_error_panic_hook` already\n * installed in Rust, which writes the actual panic site (file:line) to\n * `console.error` before the wasm trap fires. Without this Node-side\n * capture, that message is lost and we are stuck guessing among the\n * multiple Rust panic classes that all surface as bare `unreachable`\n * (Invariant #14). The dynamic `await import('./services/serve.js')`\n * below is the first thing that pulls in `@loro-extended/repo` which in\n * turn loads the wasm module — so installing here is well-ordered.\n */\ninstallWasmPanicBuffer();\n\nfunction getVersion(): string {\n try {\n const thisFile = fileURLToPath(import.meta.url);\n const pkgPath = resolve(thisFile, '../../package.json');\n const pkg: { version?: string } = JSON.parse(readFileSync(pkgPath, 'utf-8'));\n return pkg.version ?? 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\ninterface CliArgs {\n serve?: boolean;\n}\n\nfunction parseCliArgs(): CliArgs {\n const { values } = parseArgs({\n options: {\n serve: { type: 'boolean', short: 's' },\n version: { type: 'boolean', short: 'v' },\n help: { type: 'boolean', short: 'h' },\n },\n strict: true,\n });\n\n if (values.version) {\n process.stdout.write(`${getVersion()}\\n`);\n process.exit(0);\n }\n\n if (values.help) {\n logger.info(\n [\n `shipyard v${getVersion()} - Ship together with AI — locally, visibly, confidently`,\n '',\n 'Usage:',\n ' shipyard start [--code CODE] Start daemon (authenticates if needed, opens browser)',\n ' shipyard login Authenticate with Shipyard',\n ' shipyard login --check Check current auth status',\n ' shipyard logout Clear stored credentials',\n '',\n 'Options:',\n ' -v, --version Show version',\n ' -h, --help Show this help',\n '',\n 'Authentication:',\n ' Run `claude auth login` to authenticate with Anthropic (primary method).',\n ' Alternatively, set ANTHROPIC_API_KEY for CI/headless environments.',\n '',\n 'Environment:',\n ' ANTHROPIC_API_KEY API key for Claude (optional, overrides OAuth)',\n ' SHIPYARD_DEV Set to 1 for dev mode (uses ~/.shipyard-dev/)',\n ' SHIPYARD_WEB_URL Override browser URL for auto-open',\n ' LOG_LEVEL Log level: debug, info, warn, error (default: info)',\n ' SHIPYARD_SIGNALING_URL Signaling server WebSocket URL (optional)',\n ' SHIPYARD_USER_TOKEN JWT for signaling auth (optional)',\n ' SHIPYARD_USER_ID User ID for signaling path (optional, from login)',\n ' SHIPYARD_MACHINE_ID Machine identifier (default: os.hostname())',\n ' SHIPYARD_MACHINE_NAME Human-readable machine name (default: os.hostname())',\n ].join('\\n')\n );\n process.exit(0);\n }\n\n return {\n serve: values.serve,\n };\n}\n\nasync function loadAuthFromConfig(env: Env): Promise<void> {\n if (env.SHIPYARD_USER_TOKEN) return;\n\n const { loadAuthToken } = await import('./services/bootstrap/auth.js');\n const auth = await loadAuthToken();\n\n if (auth.status === 'ok') {\n env.SHIPYARD_USER_TOKEN = auth.token;\n env.SHIPYARD_USER_ID = auth.userId;\n env.SHIPYARD_USER_DISPLAY_NAME = auth.displayName;\n if (auth.signalingUrl) {\n env.SHIPYARD_SIGNALING_URL = auth.signalingUrl;\n }\n return;\n }\n\n if (auth.status === 'expired') {\n logger.warn('Auth token expired. Run `shipyard login` to re-authenticate.');\n return;\n }\n\n logger.warn('No auth token found. Run `shipyard login` to authenticate.');\n}\n\nasync function handleSubcommand(): Promise<boolean> {\n const subcommand = process.argv[2];\n\n if (subcommand === 'login') {\n const { loginCommand } = await import('./shared/commands/login.js');\n const hasCheck = process.argv.includes('--check');\n await loginCommand({ check: hasCheck });\n return true;\n }\n\n if (subcommand === 'logout') {\n const { logoutCommand } = await import('./shared/commands/logout.js');\n await logoutCommand();\n return true;\n }\n\n if (subcommand === 'start') {\n const { startCommand } = await import('./shared/commands/start.js');\n await startCommand();\n return true;\n }\n\n if (subcommand === 'roi') {\n const { roiCommand } = await import('./shared/commands/roi.js');\n await roiCommand();\n return true;\n }\n\n return false;\n}\n\nasync function main(): Promise<void> {\n if (await handleSubcommand()) return;\n\n const env = validateEnv();\n const args = parseCliArgs();\n\n if (args.serve) {\n await loadAuthFromConfig(env);\n const { serve } = await import('./services/serve.js');\n return serve({ isDev: env.SHIPYARD_DEV });\n }\n\n logger.error('Use `shipyard start` to run the daemon. Use --help for usage.');\n process.exit(1);\n}\n\nmain().catch((error: unknown) => {\n const errMsg = error instanceof Error ? error.message : String(error);\n const errStack = error instanceof Error ? error.stack : undefined;\n logger.error({ err: errMsg, stack: errStack }, 'Fatal error');\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
@@ -3,10 +3,10 @@ import {
3
3
  ensureAuthenticated,
4
4
  getSignalingUrl,
5
5
  loginCommand
6
- } from "./chunk-OHIIVBEZ.js";
7
- import "./chunk-CCW5QAUH.js";
8
- import "./chunk-UWBX6UGC.js";
6
+ } from "./chunk-5J6ABOPF.js";
7
+ import "./chunk-6GPDEZNR.js";
9
8
  import "./chunk-EHQITHQX.js";
9
+ import "./chunk-CCW5QAUH.js";
10
10
  import "./chunk-GLH3V7NG.js";
11
11
  import "./chunk-DNIC3FOH.js";
12
12
  import "./chunk-M5M6VC5F.js";
@@ -16,4 +16,4 @@ export {
16
16
  getSignalingUrl,
17
17
  loginCommand
18
18
  };
19
- //# sourceMappingURL=login-N43V35KE.js.map
19
+ //# sourceMappingURL=login-D3OUX3KD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}