git-sich 0.0.0-stage → 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -2
  3. package/dist/cli.js +1570 -0
  4. package/package.json +42 -3
package/dist/cli.js ADDED
@@ -0,0 +1,1570 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { realpathSync as realpathSync3, statSync } from "node:fs";
5
+ import { resolve as resolve6 } from "node:path";
6
+ // package.json
7
+ var version = "0.1.0";
8
+
9
+ // src/ui.ts
10
+ class SichError extends Error {
11
+ }
12
+ function fail(message) {
13
+ throw new SichError(message);
14
+ }
15
+ var colorOut = !!process.stdout.isTTY && !process.env.NO_COLOR;
16
+ var colorErr = colorOut && !!process.stderr.isTTY;
17
+ function painter(enabled) {
18
+ const wrap = (code) => (s) => enabled ? `\x1B[${code}m${s}\x1B[0m` : s;
19
+ return {
20
+ bold: wrap("1"),
21
+ dim: wrap("2"),
22
+ red: wrap("31"),
23
+ green: wrap("32"),
24
+ yellow: wrap("33"),
25
+ cyan: wrap("36")
26
+ };
27
+ }
28
+ var c = painter(colorOut);
29
+ var ce = painter(colorErr);
30
+ function out(line = "") {
31
+ process.stdout.write(line + `
32
+ `);
33
+ }
34
+ function note(line) {
35
+ out(c.dim(line));
36
+ }
37
+ function warn(message) {
38
+ process.stderr.write(`${ce.yellow("warning:")} ${message}
39
+ `);
40
+ }
41
+ function printError(message) {
42
+ process.stderr.write(`${ce.red("sich:")} ${message}
43
+ `);
44
+ }
45
+ function plural(n, word, pluralWord = word + "s") {
46
+ return `${n} ${n === 1 ? word : pluralWord}`;
47
+ }
48
+ function shellQuote(s) {
49
+ return /^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
50
+ }
51
+ function listSome(items, max = 5) {
52
+ if (items.length <= max)
53
+ return items.join(", ");
54
+ return `${items.slice(0, max).join(", ")} and ${items.length - max} more`;
55
+ }
56
+
57
+ // src/args.ts
58
+ function parseArgs(argv, spec, aliases = {}) {
59
+ const flags = {};
60
+ const positionals = [];
61
+ for (let i = 0;i < argv.length; i++) {
62
+ const arg = argv[i];
63
+ if (arg === "--") {
64
+ positionals.push(...argv.slice(i + 1));
65
+ break;
66
+ }
67
+ if (!arg.startsWith("-") || arg === "-") {
68
+ positionals.push(arg);
69
+ continue;
70
+ }
71
+ let name = arg;
72
+ let value;
73
+ const eq = arg.indexOf("=");
74
+ if (arg.startsWith("--") && eq > 0) {
75
+ name = arg.slice(0, eq);
76
+ value = arg.slice(eq + 1);
77
+ }
78
+ name = aliases[name] ?? name;
79
+ const kind = spec[name];
80
+ if (!kind)
81
+ fail(`unknown option '${arg}'`);
82
+ if (kind === "bool") {
83
+ if (value !== undefined)
84
+ fail(`option '${name}' takes no value`);
85
+ flags[name] = true;
86
+ } else if (kind === "value") {
87
+ if (value === undefined) {
88
+ value = argv[++i];
89
+ if (value === undefined)
90
+ fail(`option '${name}' needs a value`);
91
+ }
92
+ flags[name] = value;
93
+ } else {
94
+ const next = argv[i + 1];
95
+ if (value === undefined && next !== undefined && !next.startsWith("-")) {
96
+ value = next;
97
+ i++;
98
+ }
99
+ flags[name] = value ?? true;
100
+ }
101
+ }
102
+ return { flags, positionals };
103
+ }
104
+ function str(p, name) {
105
+ const v = p.flags[name];
106
+ return typeof v === "string" ? v : undefined;
107
+ }
108
+ function wantsHelp(argv) {
109
+ for (const a of argv) {
110
+ if (a === "--")
111
+ return false;
112
+ if (a === "--help" || a === "-h")
113
+ return true;
114
+ }
115
+ return false;
116
+ }
117
+
118
+ // src/claims.ts
119
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
120
+ var isDirClaim = (c2) => c2.endsWith("/");
121
+ var bare = (c2) => isDirClaim(c2) ? c2.slice(0, -1) : c2;
122
+ function samePath(a, b) {
123
+ return bare(a) === bare(b);
124
+ }
125
+ function isInside(p, dir) {
126
+ return bare(p).startsWith(bare(dir) + "/");
127
+ }
128
+ function covers(claim, p) {
129
+ return samePath(claim, p) || isDirClaim(claim) && isInside(p, claim);
130
+ }
131
+ function cleanClaim(raw) {
132
+ let p = raw.trim();
133
+ while (p.startsWith("./"))
134
+ p = p.slice(2);
135
+ if (p.startsWith("/"))
136
+ p = p.replace(/^\/+/, "");
137
+ const dir = p.endsWith("/");
138
+ const segs = bare(p).split("/");
139
+ if (!p || segs.some((s) => s === "" || s === "." || s === "..") || p.includes("\x00"))
140
+ return null;
141
+ if (segs[0] === ".git" || segs[0] === ".sich")
142
+ return null;
143
+ return segs.join("/") + (dir ? "/" : "");
144
+ }
145
+ function storable(claim) {
146
+ return !/[\r\n]/.test(claim) && !claim.startsWith("#") && cleanClaim(claim) === claim;
147
+ }
148
+ function normalizeClaims(claims) {
149
+ const unique = [...new Set(claims)];
150
+ const kept = unique.filter((c2) => !(!isDirClaim(c2) && unique.includes(c2 + "/")));
151
+ return kept.filter((c2) => !kept.some((d) => d !== c2 && isDirClaim(d) && isInside(c2, d))).sort();
152
+ }
153
+ var warned = new Set;
154
+ function warnOnce(message) {
155
+ if (warned.has(message))
156
+ return;
157
+ warned.add(message);
158
+ warn(message);
159
+ }
160
+ function readManifest(file, layer) {
161
+ if (!existsSync(file))
162
+ return { claims: [], comments: [] };
163
+ const claims = [];
164
+ const comments = [];
165
+ for (const line of readFileSync(file, "utf8").split(`
166
+ `)) {
167
+ const t = line.trim();
168
+ if (!t)
169
+ continue;
170
+ if (t.startsWith("#")) {
171
+ comments.push(line.trimEnd());
172
+ continue;
173
+ }
174
+ const claim = cleanClaim(t);
175
+ if (claim)
176
+ claims.push(claim);
177
+ else
178
+ warnOnce(`${layer}: ignoring invalid claim ${JSON.stringify(t)} in .sich/${layer}.paths`);
179
+ }
180
+ return { claims: normalizeClaims(claims), comments };
181
+ }
182
+ function writeManifest(file, m) {
183
+ const body = [...m.comments, ...normalizeClaims(m.claims)];
184
+ writeFileSync(file, body.length ? body.join(`
185
+ `) + `
186
+ ` : "");
187
+ }
188
+ function ownerOf(all, p, icase = false) {
189
+ const fold = (s) => icase ? s.toLowerCase() : s;
190
+ const path = fold(p);
191
+ let best = null;
192
+ for (const [layer, claims] of all) {
193
+ for (const claim of claims) {
194
+ if (covers(fold(claim), path) && (!best || bare(claim).length > bare(best.claim).length)) {
195
+ best = { layer, claim };
196
+ }
197
+ }
198
+ }
199
+ return best;
200
+ }
201
+
202
+ // src/context.ts
203
+ import { existsSync as existsSync3, lstatSync, readdirSync, realpathSync } from "node:fs";
204
+ import { basename, dirname as dirname2, isAbsolute, join, relative as relative2, resolve as resolve2, sep } from "node:path";
205
+
206
+ // src/excludes.ts
207
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
208
+ import { dirname, relative, resolve } from "node:path";
209
+
210
+ // src/git.ts
211
+ import { spawnSync } from "node:child_process";
212
+ var STRIPPED_ENV = [
213
+ "GIT_DIR",
214
+ "GIT_WORK_TREE",
215
+ "GIT_INDEX_FILE",
216
+ "GIT_OBJECT_DIRECTORY",
217
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
218
+ "GIT_COMMON_DIR",
219
+ "GIT_PREFIX",
220
+ "GIT_NAMESPACE",
221
+ "GIT_QUARANTINE_PATH",
222
+ "GIT_IMPLICIT_WORK_TREE",
223
+ "GIT_INTERNAL_SUPER_PREFIX",
224
+ "GIT_LITERAL_PATHSPECS",
225
+ "GIT_GLOB_PATHSPECS",
226
+ "GIT_NOGLOB_PATHSPECS",
227
+ "GIT_ICASE_PATHSPECS"
228
+ ];
229
+ function cleanEnv(extra = {}) {
230
+ const env = {};
231
+ for (const [k, v] of Object.entries(process.env)) {
232
+ if (v !== undefined && !STRIPPED_ENV.includes(k))
233
+ env[k] = v;
234
+ }
235
+ return { ...env, ...extra };
236
+ }
237
+ function spawn(cmd, cwd, env, inherit) {
238
+ const r = spawnSync(cmd[0], cmd.slice(1), {
239
+ cwd,
240
+ env,
241
+ encoding: "utf8",
242
+ stdio: inherit ? "inherit" : ["ignore", "pipe", "pipe"],
243
+ maxBuffer: 1024 ** 3
244
+ });
245
+ if (r.error)
246
+ fail(`cannot run ${cmd[0]}: ${r.error.message}`);
247
+ return { code: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
248
+ }
249
+ function repoArgs(repo) {
250
+ return [`--git-dir=${repo.gitDir}`, `--work-tree=${repo.workTree}`];
251
+ }
252
+ function git(repo, args, opts = {}) {
253
+ const literal = opts.literal ?? true;
254
+ const env = cleanEnv({ GIT_LITERAL_PATHSPECS: literal ? "1" : "0", GIT_OPTIONAL_LOCKS: "0", ...opts.env });
255
+ const result = spawn(["git", ...repoArgs(repo), ...args], opts.cwd ?? repo.workTree, env, false);
256
+ if (result.code !== 0 && !opts.allowFail) {
257
+ const msg = result.stderr.trim().split(`
258
+ `).filter(Boolean).pop() ?? `exit code ${result.code}`;
259
+ fail(`${repo.name}: git ${args[0]} failed: ${msg}`);
260
+ }
261
+ return result;
262
+ }
263
+ function gitInherit(repo, args, cwd) {
264
+ return spawn(["git", ...repoArgs(repo), ...args], cwd ?? repo.workTree, cleanEnv(), true).code;
265
+ }
266
+ function gitPlain(args, cwd) {
267
+ return spawn(["git", ...args], cwd, cleanEnv(), false);
268
+ }
269
+ function splitZ(s) {
270
+ return s.split("\x00").filter((x) => x !== "");
271
+ }
272
+ function lines(s) {
273
+ return s.split(`
274
+ `).filter((x) => x !== "");
275
+ }
276
+ function trackedUnder(repo, paths) {
277
+ if (paths.length === 0)
278
+ return [];
279
+ return splitZ(git(repo, ["ls-files", "-z", "--cached", "--", ...paths]).stdout);
280
+ }
281
+ function isIgnored(repo, path, noIndex = false) {
282
+ const args = ["check-ignore", "-q", ...noIndex ? ["--no-index"] : [], "--", path];
283
+ return git(repo, args, { allowFail: true, literal: false }).code === 0;
284
+ }
285
+ function currentBranch(repo) {
286
+ const r = git(repo, ["symbolic-ref", "--short", "-q", "HEAD"], { allowFail: true });
287
+ return r.code === 0 ? r.stdout.trim() : null;
288
+ }
289
+ function upstreamOf(repo) {
290
+ const r = git(repo, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], { allowFail: true });
291
+ return r.code === 0 && r.stdout.trim() ? r.stdout.trim() : null;
292
+ }
293
+ function remotesOf(repo) {
294
+ return lines(git(repo, ["remote"]).stdout);
295
+ }
296
+
297
+ // src/excludes.ts
298
+ var BLOCK_BEGIN = "# >>> sich: managed, do not edit >>>";
299
+ var BLOCK_END = "# <<< sich <<<";
300
+ function escapePattern(p) {
301
+ return p.replace(/[\\*?[]/g, (m) => "\\" + m).replace(/ $/, "\\ ");
302
+ }
303
+ function baseExcludeLines(allClaims) {
304
+ const patterns = [...new Set(allClaims)].sort().map((c2) => "/" + escapePattern(c2));
305
+ return ["/.sich/", ...patterns];
306
+ }
307
+ function layerExcludeLines(layer, own, others) {
308
+ const root = { kind: "parent", children: new Map };
309
+ const insert = (claim) => {
310
+ const segs = bare(claim).split("/");
311
+ let node = root;
312
+ for (let i = 0;i < segs.length; i++) {
313
+ const seg = segs[i];
314
+ let child = node.children.get(seg);
315
+ if (!child) {
316
+ child = { kind: "parent", children: new Map };
317
+ node.children.set(seg, child);
318
+ }
319
+ if (child.kind !== "parent")
320
+ return;
321
+ if (i === segs.length - 1) {
322
+ child.kind = isDirClaim(claim) ? "dir" : "file";
323
+ child.children.clear();
324
+ }
325
+ node = child;
326
+ }
327
+ };
328
+ const ownWithManifest = [...own, `.sich/${layer}.paths`];
329
+ [...ownWithManifest].sort((a, b) => bare(a).split("/").length - bare(b).split("/").length).forEach(insert);
330
+ const out2 = ["/*"];
331
+ const emit = (node, prefix) => {
332
+ for (const name of [...node.children.keys()].sort()) {
333
+ const child = node.children.get(name);
334
+ const p = escapePattern(prefix + name);
335
+ if (child.kind === "dir")
336
+ out2.push(`!/${p}/`);
337
+ else if (child.kind === "file")
338
+ out2.push(`!/${p}`);
339
+ else {
340
+ out2.push(`!/${p}/`, `/${p}/*`);
341
+ emit(child, prefix + name + "/");
342
+ }
343
+ }
344
+ };
345
+ emit(root, "");
346
+ const carve = [...new Set(others)].filter((o) => !own.some((c2) => samePath(o, c2) || isInside(c2, o))).sort();
347
+ for (const o of carve)
348
+ out2.push("/" + escapePattern(o));
349
+ return out2;
350
+ }
351
+ function spliceBlock(content, body) {
352
+ const block = [BLOCK_BEGIN, ...body, BLOCK_END].join(`
353
+ `) + `
354
+ `;
355
+ const start = content.indexOf(BLOCK_BEGIN);
356
+ if (start === -1) {
357
+ const pre = content && !content.endsWith(`
358
+ `) ? content + `
359
+ ` : content;
360
+ return pre + block;
361
+ }
362
+ const end = content.indexOf(BLOCK_END, start);
363
+ let after = end === -1 ? "" : content.slice(end + BLOCK_END.length);
364
+ if (after.startsWith(`
365
+ `))
366
+ after = after.slice(1);
367
+ return content.slice(0, start) + block + after;
368
+ }
369
+ function excludeFile(repo) {
370
+ const p = git(repo, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim();
371
+ return resolve(repo.workTree, p);
372
+ }
373
+ function excludeFileShown(repo) {
374
+ return relative(repo.workTree, excludeFile(repo));
375
+ }
376
+ function describeExclude(t) {
377
+ const file = excludeFileShown(t.repo);
378
+ if (t.repo.name !== "base")
379
+ return `${t.repo.name} ignores everything except its claims -> ${file}`;
380
+ const claims = t.lines.length - 1;
381
+ return `excluded .sich/${claims ? ` and ${plural(claims, "private path")}` : ""} from base -> ${file}`;
382
+ }
383
+ function applyExclude(t, write) {
384
+ const file = excludeFile(t.repo);
385
+ const current = existsSync2(file) ? readFileSync2(file, "utf8") : "";
386
+ const next = spliceBlock(current, t.lines);
387
+ if (next === current)
388
+ return false;
389
+ if (write) {
390
+ mkdirSync(dirname(file), { recursive: true });
391
+ writeFileSync2(file, next);
392
+ }
393
+ return true;
394
+ }
395
+
396
+ // src/context.ts
397
+ var COMMANDS = [
398
+ "init",
399
+ "new",
400
+ "attach",
401
+ "add",
402
+ "rm",
403
+ "which",
404
+ "ls",
405
+ "status",
406
+ "commit",
407
+ "pull",
408
+ "push",
409
+ "sync",
410
+ "check",
411
+ "help"
412
+ ];
413
+ var LAYER_NAME = /^[a-z0-9][a-z0-9._-]*$/;
414
+ function validateLayerName(name) {
415
+ if (name === "base" || COMMANDS.includes(name)) {
416
+ fail(`'${name}' is reserved and cannot be a layer name`);
417
+ }
418
+ if (!LAYER_NAME.test(name)) {
419
+ fail(`invalid layer name '${name}' (use lowercase letters, digits, '.', '_' or '-', starting with a letter or digit)`);
420
+ }
421
+ if (name.endsWith(".paths") || name.endsWith(".lock"))
422
+ fail(`invalid layer name '${name}'`);
423
+ }
424
+ function loadCtx(cwd) {
425
+ let dir = cwd;
426
+ while (!existsSync3(join(dir, ".git"))) {
427
+ const up = dirname2(dir);
428
+ if (up === dir)
429
+ fail("not inside a git repository (sich needs a base repo: run git init first)");
430
+ dir = up;
431
+ }
432
+ const r = gitPlain(["rev-parse", "--absolute-git-dir"], dir);
433
+ if (r.code !== 0)
434
+ fail(`cannot read the git repository at ${dir}: ${r.stderr.trim()}`);
435
+ const root = dir;
436
+ return {
437
+ cwd,
438
+ root,
439
+ base: { name: "base", gitDir: r.stdout.trim(), workTree: root },
440
+ sichDir: join(root, ".sich")
441
+ };
442
+ }
443
+ function isInitialized(ctx) {
444
+ return existsSync3(ctx.sichDir);
445
+ }
446
+ function requireInit(ctx) {
447
+ if (!isInitialized(ctx))
448
+ fail(`sich is not initialized in ${ctx.root} (run: sich init)`);
449
+ }
450
+ function layerRepo(ctx, name) {
451
+ return { name, gitDir: join(ctx.sichDir, name), workTree: ctx.root };
452
+ }
453
+ function manifestPath(ctx, layer) {
454
+ return join(ctx.sichDir, `${layer}.paths`);
455
+ }
456
+ function manifestRel(layer) {
457
+ return `.sich/${layer}.paths`;
458
+ }
459
+ function layerNames(ctx) {
460
+ if (!existsSync3(ctx.sichDir))
461
+ return [];
462
+ return readdirSync(ctx.sichDir, { withFileTypes: true }).filter((e) => e.isDirectory() && LAYER_NAME.test(e.name) && existsSync3(join(ctx.sichDir, e.name, "HEAD"))).map((e) => e.name).sort();
463
+ }
464
+ function layerExists(ctx, name) {
465
+ return layerNames(ctx).includes(name);
466
+ }
467
+ function requireLayer(ctx, name) {
468
+ if (!name)
469
+ fail("missing layer name");
470
+ if (name === "base")
471
+ fail("'base' is the public repo, not a layer");
472
+ if (!layerExists(ctx, name))
473
+ fail(`no such layer '${name}' (layers: ${layerNames(ctx).join(", ") || "none"})`);
474
+ return layerRepo(ctx, name);
475
+ }
476
+ function repoByName(ctx, name) {
477
+ if (name === "base")
478
+ return ctx.base;
479
+ return requireLayer(ctx, name);
480
+ }
481
+ function allClaims(ctx) {
482
+ const m = new Map;
483
+ for (const l of layerNames(ctx))
484
+ m.set(l, readManifest(manifestPath(ctx, l), l).claims);
485
+ return m;
486
+ }
487
+ function excludeTargets(ctx) {
488
+ const claims = allClaims(ctx);
489
+ const everything = [...claims.values()].flat();
490
+ const targets = [{ repo: ctx.base, lines: baseExcludeLines(everything) }];
491
+ for (const [layer, own] of claims) {
492
+ const others = [...claims].filter(([l]) => l !== layer).flatMap(([, cs]) => cs);
493
+ targets.push({ repo: layerRepo(ctx, layer), lines: layerExcludeLines(layer, own, others) });
494
+ }
495
+ return targets;
496
+ }
497
+ function syncExcludes(ctx) {
498
+ if (!isInitialized(ctx))
499
+ return;
500
+ for (const t of excludeTargets(ctx))
501
+ applyExclude(t, true);
502
+ }
503
+ function ignoresCase(ctx) {
504
+ const r = git(ctx.base, ["config", "--type=bool", "--get", "core.ignorecase"], { allowFail: true });
505
+ return r.stdout.trim() === "true";
506
+ }
507
+ function onDiskName(dir, name) {
508
+ try {
509
+ lstatSync(join(dir, name));
510
+ const entries = readdirSync(dir);
511
+ if (entries.includes(name))
512
+ return name;
513
+ return entries.find((e) => e.toLowerCase() === name.toLowerCase()) ?? name;
514
+ } catch {
515
+ return name;
516
+ }
517
+ }
518
+ function realParent(abs) {
519
+ const parent = dirname2(abs);
520
+ if (parent === abs)
521
+ return abs;
522
+ let real;
523
+ try {
524
+ real = realpathSync.native(parent);
525
+ } catch {
526
+ real = realParent(parent);
527
+ }
528
+ return join(real, onDiskName(real, basename(abs)));
529
+ }
530
+ function resolveUserPath(ctx, input, opts = {}) {
531
+ const abs = realParent(resolve2(ctx.cwd, input));
532
+ const rel = relative2(ctx.root, abs);
533
+ if (rel === "")
534
+ fail(`'${input}' is the repository root`);
535
+ if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel))
536
+ fail(`'${input}' is outside the repository`);
537
+ const posix = rel.split(sep).join("/");
538
+ const first = posix.split("/")[0];
539
+ if (first === ".git" || first === ".sich" && !opts.allowSich)
540
+ fail(`'${input}' is inside ${first}/`);
541
+ let exists = false;
542
+ let isDir = false;
543
+ try {
544
+ const st = lstatSync(abs);
545
+ exists = true;
546
+ isDir = st.isDirectory();
547
+ } catch {}
548
+ return { rel: posix, exists, isDir };
549
+ }
550
+ function collisions(ctx, paths) {
551
+ const hits = new Set;
552
+ const kind = (rel) => {
553
+ try {
554
+ return lstatSync(join(ctx.root, rel)).isDirectory() ? "dir" : "file";
555
+ } catch {
556
+ return null;
557
+ }
558
+ };
559
+ for (const p of paths) {
560
+ if (kind(p))
561
+ hits.add(p);
562
+ const segs = p.split("/");
563
+ for (let i = 1;i < segs.length; i++) {
564
+ const parent = segs.slice(0, i).join("/");
565
+ if (kind(parent) === "file")
566
+ hits.add(parent);
567
+ }
568
+ }
569
+ return [...hits].sort();
570
+ }
571
+
572
+ // src/commands/add.ts
573
+ function cmdAdd(ctx, args) {
574
+ const p = parseArgs(args, { "--move": "bool" });
575
+ const [layer, ...inputs] = p.positionals;
576
+ if (!layer || inputs.length === 0)
577
+ fail("usage: sich add <layer> <path...> [--move]");
578
+ const move = p.flags["--move"] === true;
579
+ const repo = requireLayer(ctx, layer);
580
+ const claims = allClaims(ctx);
581
+ const own = claims.get(layer) ?? [];
582
+ const icase = ignoresCase(ctx);
583
+ const fold = (s) => icase ? s.toLowerCase() : s;
584
+ const targets = [];
585
+ for (const input of inputs) {
586
+ const up = resolveUserPath(ctx, input);
587
+ if (!up.exists)
588
+ fail(`'${input}' does not exist`);
589
+ const claim = up.isDir ? up.rel + "/" : up.rel;
590
+ if (!storable(claim)) {
591
+ fail(`cannot claim '${up.rel}': claims can't start with '#', start or end with whitespace, ` + `or contain line breaks (rename it, or claim a directory containing it)`);
592
+ }
593
+ if (!targets.includes(claim))
594
+ targets.push(claim);
595
+ }
596
+ const moves = [];
597
+ for (const t of targets) {
598
+ const inBase = trackedUnder(ctx.base, [t]);
599
+ if (inBase.length) {
600
+ if (!move) {
601
+ fail(`${t} is tracked by base (${listSome(inBase, 3)}); use --move to move it into ${layer}`);
602
+ }
603
+ moves.push({ from: ctx.base, claim: t, files: inBase, dropClaims: [] });
604
+ }
605
+ for (const other of layerNames(ctx)) {
606
+ if (other === layer)
607
+ continue;
608
+ const theirs = claims.get(other) ?? [];
609
+ const outer = theirs.find((cl) => isDirClaim(cl) && isInside(fold(t), fold(cl)));
610
+ if (outer) {
611
+ fail(`${t} is inside ${outer}, claimed by layer ${other}; claims can't nest across layers ` + `(to move all of it: sich add ${layer} ${outer} --move)`);
612
+ }
613
+ const inner = theirs.filter((cl) => covers(fold(t), fold(cl)));
614
+ const tracked = trackedUnder(layerRepo(ctx, other), [t]);
615
+ if (inner.length === 0 && tracked.length === 0)
616
+ continue;
617
+ if (!move) {
618
+ const why = inner.some((cl) => samePath(fold(cl), fold(t))) ? `is claimed by layer ${other}` : inner.length ? `contains ${listSome(inner, 3)}, claimed by layer ${other}` : `is tracked by layer ${other}`;
619
+ fail(`${t} ${why}; use --move to move it into ${layer}`);
620
+ }
621
+ moves.push({ from: layerRepo(ctx, other), claim: t, files: tracked, dropClaims: inner });
622
+ }
623
+ }
624
+ for (const m of moves) {
625
+ if (m.files.length)
626
+ git(m.from, ["rm", "--cached", "-q", "--", ...m.files]);
627
+ if (m.dropClaims.length) {
628
+ const file2 = manifestPath(ctx, m.from.name);
629
+ const man2 = readManifest(file2, m.from.name);
630
+ writeManifest(file2, { ...man2, claims: man2.claims.filter((cl) => !m.dropClaims.includes(cl)) });
631
+ git(m.from, ["add", "-f", "--", manifestRel(m.from.name)]);
632
+ }
633
+ if (m.from.name === "base") {
634
+ warn(`moved ${m.claim} out of base: its removal is staged in base, commit it there (git commit). ` + `Old versions remain in base's history (and on its remote, which may be public). ` + `Rewrite that history and rotate any secrets if that matters.`);
635
+ } else {
636
+ const changes = [
637
+ m.dropClaims.length && `dropped its ${plural(m.dropClaims.length, "claim")} ${listSome(m.dropClaims)} -> ${manifestRel(m.from.name)}`,
638
+ m.files.length && `untracked ${plural(m.files.length, "file")} there`
639
+ ].filter(Boolean);
640
+ warn(`moved ${m.claim} from ${m.from.name}: ${changes.join("; ")} ` + `(commit with: sich commit ${m.from.name} -m <msg>)`);
641
+ }
642
+ }
643
+ const file = manifestPath(ctx, layer);
644
+ const man = readManifest(file, layer);
645
+ const already = targets.filter((t) => own.some((cl) => covers(cl, t)));
646
+ writeManifest(file, { ...man, claims: normalizeClaims([...man.claims, ...targets]) });
647
+ syncExcludes(ctx);
648
+ for (const t of targets)
649
+ stage(repo, t);
650
+ git(repo, ["add", "-f", "--", manifestRel(layer)]);
651
+ for (const t of already)
652
+ note(`${t} was already claimed by ${layer}`);
653
+ out(`${c.green("claimed")} ${listSome(targets)} for ${c.bold(layer)} -> ${manifestRel(layer)}`);
654
+ out(`hidden from base -> ${excludeFileShown(ctx.base)}`);
655
+ note(`staged in ${layer}; commit with: sich commit ${layer} -m <msg>`);
656
+ return 0;
657
+ }
658
+ function stage(repo, claim) {
659
+ if (!isDirClaim(claim)) {
660
+ git(repo, ["add", "-f", "--", claim]);
661
+ return;
662
+ }
663
+ const candidates = splitZ(git(repo, ["ls-files", "-z", "-c", "-o", "--exclude-standard", "--", claim]).stdout);
664
+ if (candidates.length) {
665
+ git(repo, ["add", "-A", "--", claim]);
666
+ } else if (isIgnored(repo, bare(claim))) {
667
+ warn(`${claim} is ignored by a .gitignore, so nothing in it was staged; claim individual files instead`);
668
+ } else {
669
+ note(`${claim} has no files yet; new files in it will belong to ${repo.name}`);
670
+ }
671
+ }
672
+ function cmdRm(ctx, args) {
673
+ const p = parseArgs(args, {});
674
+ const [layer, ...inputs] = p.positionals;
675
+ if (!layer || inputs.length === 0)
676
+ fail("usage: sich rm <layer> <path...>");
677
+ const repo = requireLayer(ctx, layer);
678
+ const file = manifestPath(ctx, layer);
679
+ const man = readManifest(file, layer);
680
+ const remove = [];
681
+ for (const input of inputs) {
682
+ const up = resolveUserPath(ctx, input);
683
+ const forms = up.exists ? [up.isDir ? up.rel + "/" : up.rel] : [up.rel, up.rel + "/"];
684
+ const hit = man.claims.find((cl) => forms.includes(cl));
685
+ if (!hit) {
686
+ const parent = man.claims.find((cl) => covers(cl, up.rel));
687
+ fail(`${up.rel} is not claimed by ${layer}` + (parent ? ` (it is inside the claim ${parent})` : ""));
688
+ }
689
+ if (!remove.includes(hit))
690
+ remove.push(hit);
691
+ }
692
+ writeManifest(file, { ...man, claims: man.claims.filter((cl) => !remove.includes(cl)) });
693
+ for (const cl of remove)
694
+ git(repo, ["rm", "--cached", "-r", "-q", "--ignore-unmatch", "--", cl]);
695
+ syncExcludes(ctx);
696
+ git(repo, ["add", "-f", "--", manifestRel(layer)]);
697
+ out(`${c.green("released")} ${listSome(remove)} from ${c.bold(layer)} -> ${manifestRel(layer)}`);
698
+ out(`no longer hidden from base -> ${excludeFileShown(ctx.base)}`);
699
+ note(`staged in ${layer}; commit with: sich commit ${layer} -m <msg>`);
700
+ note(`${plural(remove.length, "path")} left on disk; they now show as untracked in base (add, delete or .gitignore them)`);
701
+ return 0;
702
+ }
703
+
704
+ // src/commands/check.ts
705
+ import { realpathSync as realpathSync2 } from "node:fs";
706
+ import { basename as basename2, dirname as dirname3, resolve as resolve3 } from "node:path";
707
+ function hookIndexEnv(ctx) {
708
+ const raw = process.env.GIT_INDEX_FILE;
709
+ if (!raw)
710
+ return {};
711
+ try {
712
+ const abs = resolve3(raw);
713
+ const real = resolve3(realpathSync2(dirname3(abs)), basename2(abs));
714
+ const gitDir = realpathSync2(ctx.base.gitDir);
715
+ return real.startsWith(gitDir + "/") ? { GIT_INDEX_FILE: real } : {};
716
+ } catch {
717
+ return {};
718
+ }
719
+ }
720
+ function cmdCheck(ctx, args) {
721
+ const p = parseArgs(args, { "--fix": "bool", "--staged": "bool" });
722
+ if (p.positionals.length)
723
+ fail("usage: sich check [--fix] [--staged]");
724
+ if (!isInitialized(ctx)) {
725
+ if (p.flags["--staged"])
726
+ return 0;
727
+ requireInit(ctx);
728
+ }
729
+ const problems = [];
730
+ const stale = [];
731
+ const fixed = [];
732
+ const hints = [];
733
+ const claims = allClaims(ctx);
734
+ const layers = layerNames(ctx);
735
+ const indexEnv = hookIndexEnv(ctx);
736
+ const icase = ignoresCase(ctx);
737
+ const fold = (s) => icase ? s.toLowerCase() : s;
738
+ const owner = (f) => ownerOf(claims, f, icase);
739
+ const isSich = (f) => fold(f).startsWith(".sich/");
740
+ const flat = [...claims].flatMap(([layer, cs]) => cs.map((claim) => ({ layer, claim })));
741
+ for (const [i, a] of flat.entries()) {
742
+ for (const b of flat.slice(i + 1)) {
743
+ if (a.layer === b.layer)
744
+ continue;
745
+ const [x, y] = [fold(a.claim), fold(b.claim)];
746
+ if (samePath(x, y)) {
747
+ problems.push(`${bare(a.claim)} is claimed by more than one layer: ${a.layer}, ${b.layer}`);
748
+ } else if (covers(x, y) || covers(y, x)) {
749
+ const [outer, inner] = covers(x, y) ? [a, b] : [b, a];
750
+ problems.push(`${inner.claim} (${inner.layer}) is nested inside ${outer.claim} (${outer.layer}); ` + `claims can't nest across layers (fix: sich rm ${inner.layer} ${shellQuote(inner.claim)})`);
751
+ }
752
+ }
753
+ }
754
+ const trackedBy = new Map;
755
+ for (const layer of layers) {
756
+ for (const f of splitZ(git(layerRepo(ctx, layer), ["ls-files", "-z"]).stdout)) {
757
+ if (f === manifestRel(layer))
758
+ continue;
759
+ trackedBy.set(f, [...trackedBy.get(f) ?? [], layer]);
760
+ }
761
+ }
762
+ for (const [f, ls] of trackedBy) {
763
+ const by = owner(f)?.layer;
764
+ if (ls.length > 1) {
765
+ problems.push(`${f} is tracked by more than one layer: ${ls.join(", ")}${by ? ` (owner: ${by})` : ""}`);
766
+ } else if (by !== ls[0]) {
767
+ const why = by ? `it belongs to ${by}` : "none of its claims cover it";
768
+ problems.push(`${ls[0]} tracks ${f} but ${why} (fix: sich ${ls[0]} rm --cached -- ${shellQuote(f)})`);
769
+ }
770
+ }
771
+ const reported = new Set;
772
+ if (p.flags["--staged"]) {
773
+ const staged = splitZ(git(ctx.base, ["diff", "--cached", "--name-only", "--no-renames", "--diff-filter=d", "-z"], { env: indexEnv }).stdout);
774
+ const stagedPrivate = staged.filter((f) => isSich(f) || owner(f));
775
+ for (const f of stagedPrivate) {
776
+ const by = owner(f);
777
+ problems.push(`base has a staged change to private path ${f}${by ? ` (claimed by ${by.layer})` : ""}`);
778
+ reported.add(f);
779
+ }
780
+ if (stagedPrivate.length) {
781
+ hints.push(`unstage with: git restore --staged -- ${stagedPrivate.map(shellQuote).join(" ")}`);
782
+ }
783
+ }
784
+ let sichFiles = 0;
785
+ for (const f of splitZ(git(ctx.base, ["ls-files", "-z"], { env: indexEnv }).stdout)) {
786
+ if (reported.has(f))
787
+ continue;
788
+ if (isSich(f)) {
789
+ sichFiles++;
790
+ continue;
791
+ }
792
+ const by = owner(f);
793
+ if (by)
794
+ problems.push(`base tracks ${f}, which is claimed by ${by.layer} (fix: git rm --cached -- ${shellQuote(f)})`);
795
+ }
796
+ if (sichFiles) {
797
+ problems.push(`base tracks ${plural(sichFiles, "file")} under .sich/ (fix: git rm -r --cached -- .sich/)`);
798
+ }
799
+ for (const t of excludeTargets(ctx)) {
800
+ if (!applyExclude(t, false))
801
+ continue;
802
+ if (p.flags["--fix"]) {
803
+ applyExclude(t, true);
804
+ fixed.push(`rewrote: ${describeExclude(t)}`);
805
+ } else {
806
+ stale.push(`stale exclude rules of ${t.repo.name} in ${excludeFileShown(t.repo)} (fix: sich check --fix)`);
807
+ }
808
+ }
809
+ const hook = p.flags["--staged"] === true;
810
+ const blocking = hook ? problems : [...problems, ...stale];
811
+ for (const f of fixed)
812
+ out(`${c.green("✓")} ${f}`);
813
+ if (hook)
814
+ for (const s of stale)
815
+ warn(`${s}; not blocking this commit`);
816
+ for (const i of blocking)
817
+ out(`${c.red("✗")} ${i}`);
818
+ for (const h of hints)
819
+ out(` ${h}`);
820
+ if (blocking.length) {
821
+ out(c.red(`sich check: ${plural(blocking.length, "issue")} found`));
822
+ return 1;
823
+ }
824
+ if (!hook)
825
+ out(c.green("sich check: ok"));
826
+ return 0;
827
+ }
828
+
829
+ // src/commands/commit.ts
830
+ function cmdCommit(ctx, args) {
831
+ const p = parseArgs(args, { "-m": "value" }, { "--message": "-m" });
832
+ const message = str(p, "-m");
833
+ if (!message)
834
+ fail("usage: sich commit [repo...] -m <msg>");
835
+ const named = p.positionals.length > 0;
836
+ const names = named ? [...new Set(p.positionals)] : ["base", ...layerNames(ctx)];
837
+ const repos = names.map((n) => repoByName(ctx, n));
838
+ const claims = allClaims(ctx);
839
+ const icase = ignoresCase(ctx);
840
+ const ownerRepo = (f) => {
841
+ if (f.startsWith(".sich/"))
842
+ return layerNames(ctx).find((l) => f === manifestRel(l)) ?? null;
843
+ return ownerOf(claims, f, icase)?.layer ?? "base";
844
+ };
845
+ let committed = 0;
846
+ for (const repo of repos) {
847
+ git(repo, ["add", "-A"]);
848
+ const changed = git(repo, ["diff", "--cached", "--name-only", "--no-renames", "--diff-filter=d", "-z"]).stdout;
849
+ const stray = splitZ(changed).filter((f) => ownerRepo(f) !== repo.name);
850
+ if (stray.length) {
851
+ git(repo, ["reset", "-q", "--", ...stray]);
852
+ warn(`${repo.name}: left out ${plural(stray.length, "file")} it doesn't own: ${listSome(stray)} (see: sich check)`);
853
+ }
854
+ const files = splitZ(git(repo, ["diff", "--cached", "--name-only", "-z"]).stdout);
855
+ if (files.length === 0) {
856
+ if (named)
857
+ note(`${repo.name}: nothing to commit`);
858
+ continue;
859
+ }
860
+ const r = git(repo, ["commit", "-q", "-m", message], { allowFail: true });
861
+ if (r.code !== 0) {
862
+ process.stderr.write(r.stderr + r.stdout);
863
+ fail(`${repo.name}: commit failed (nothing committed there)`);
864
+ }
865
+ const sha = git(repo, ["rev-parse", "--short", "HEAD"]).stdout.trim();
866
+ out(`${c.bold(repo.name)}: committed ${c.yellow(sha)} (${plural(files.length, "file")}): ${listSome(files)}`);
867
+ committed++;
868
+ }
869
+ if (committed === 0 && !named)
870
+ note("nothing to commit");
871
+ return 0;
872
+ }
873
+
874
+ // src/commands/init.ts
875
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
876
+ import { join as join2, relative as relative3, resolve as resolve4 } from "node:path";
877
+ var HOOK_MARKER = /sich\S*\s+check --staged/;
878
+ var HOOK_HEADER = "# Installed by sich:";
879
+ var HOOK_LINE = '"${SICH_BIN:-sich}" check --staged || exit 1';
880
+ var HOOK_BODY = `#!/bin/sh
881
+ ${HOOK_HEADER} stop private (layer-claimed) files from being committed to base.
882
+ sich="\${SICH_BIN:-sich}"
883
+ if ! command -v "$sich" >/dev/null 2>&1; then
884
+ echo "sich: commit blocked: '$sich' not found; put sich on PATH or set SICH_BIN (skip once: git commit --no-verify)" >&2
885
+ exit 1
886
+ fi
887
+ exec "$sich" check --staged
888
+ `;
889
+ function ensureInit(ctx) {
890
+ const fresh = !isInitialized(ctx);
891
+ if (fresh) {
892
+ mkdirSync2(ctx.sichDir, { recursive: true });
893
+ out(`created ${c.bold(".sich/")} (holds every layer's git data and claims list)`);
894
+ }
895
+ for (const t of excludeTargets(ctx)) {
896
+ if (applyExclude(t, true))
897
+ out(describeExclude(t));
898
+ }
899
+ installHook(ctx);
900
+ }
901
+ function installHook(ctx) {
902
+ const hooksDir = resolve4(ctx.root, git(ctx.base, ["rev-parse", "--git-path", "hooks"]).stdout.trim());
903
+ const hook = join2(hooksDir, "pre-commit");
904
+ const shown = relative3(ctx.root, hook) || hook;
905
+ const hooksPath = git(ctx.base, ["config", "--get", "core.hooksPath"], { allowFail: true }).stdout.trim();
906
+ if (existsSync4(hook)) {
907
+ const current = readFileSync3(hook, "utf8");
908
+ if (current.includes(HOOK_HEADER)) {
909
+ if (current !== HOOK_BODY) {
910
+ writeFileSync3(hook, HOOK_BODY);
911
+ chmodSync(hook, 493);
912
+ out(`updated pre-commit hook -> ${shown}`);
913
+ }
914
+ return;
915
+ }
916
+ if (HOOK_MARKER.test(current))
917
+ return;
918
+ out(`${c.yellow("pre-commit hook exists")} (${shown}); add this line to it:`);
919
+ out(` ${HOOK_LINE}`);
920
+ return;
921
+ }
922
+ if (hooksPath) {
923
+ out(`${c.yellow("core.hooksPath is set")} (${hooksPath}); add this line to its pre-commit hook:`);
924
+ out(` ${HOOK_LINE}`);
925
+ return;
926
+ }
927
+ mkdirSync2(hooksDir, { recursive: true });
928
+ writeFileSync3(hook, HOOK_BODY);
929
+ chmodSync(hook, 493);
930
+ out(`installed pre-commit hook -> ${shown}`);
931
+ }
932
+ function cmdInit(ctx, args) {
933
+ if (parseArgs(args, {}).positionals.length)
934
+ fail("usage: sich init");
935
+ const already = isInitialized(ctx);
936
+ ensureInit(ctx);
937
+ out(already ? `sich already initialized in ${ctx.root}` : `${c.green("initialized")} sich in ${ctx.root}`);
938
+ return 0;
939
+ }
940
+
941
+ // src/commands/inspect.ts
942
+ function cmdWhich(ctx, args) {
943
+ const p = parseArgs(args, {});
944
+ const [input, ...extra] = p.positionals;
945
+ if (!input || extra.length)
946
+ fail("usage: sich which <path>");
947
+ const up = resolveUserPath(ctx, input, { allowSich: true });
948
+ const path = up.rel;
949
+ const owner = ownerOf(allClaims(ctx), path);
950
+ if (owner) {
951
+ const tracked = trackedUnder(layerRepo(ctx, owner.layer), [path]).length > 0;
952
+ out(tracked ? owner.layer : `${owner.layer} ${c.dim("(claimed, not yet committed)")}`);
953
+ return 0;
954
+ }
955
+ for (const layer of layerNames(ctx)) {
956
+ if (trackedUnder(layerRepo(ctx, layer), [path]).length) {
957
+ out(layer);
958
+ return 0;
959
+ }
960
+ }
961
+ if (trackedUnder(ctx.base, [path]).length) {
962
+ out("base");
963
+ return 0;
964
+ }
965
+ out(isIgnored(ctx.base, path, true) ? "ignored" : "untracked");
966
+ return 0;
967
+ }
968
+ function cmdLs(ctx, args) {
969
+ const p = parseArgs(args, {});
970
+ if (p.positionals.length > 1)
971
+ fail("usage: sich ls [layer]");
972
+ const only = p.positionals[0];
973
+ if (only)
974
+ requireLayer(ctx, only);
975
+ const claims = allClaims(ctx);
976
+ const layers = only ? [only] : layerNames(ctx);
977
+ if (layers.length === 0) {
978
+ note("no layers yet (create one with: sich new <layer>)");
979
+ return 0;
980
+ }
981
+ layers.forEach((layer, i) => {
982
+ if (i > 0)
983
+ out();
984
+ out(c.bold(layer));
985
+ const own = claims.get(layer) ?? [];
986
+ out(` claims:${own.length ? "" : c.dim(" (none)")}`);
987
+ for (const cl of own)
988
+ out(` ${cl}`);
989
+ const files = splitZ(git(layerRepo(ctx, layer), ["ls-files", "-z"]).stdout);
990
+ out(` tracked:${files.length ? "" : c.dim(" (none)")}`);
991
+ for (const f of files)
992
+ out(` ${f}`);
993
+ });
994
+ return 0;
995
+ }
996
+
997
+ // src/commands/layer.ts
998
+ import { spawnSync as spawnSync2 } from "node:child_process";
999
+ import { existsSync as existsSync5, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
1000
+ import { basename as basename3, isAbsolute as isAbsolute2, join as join3, resolve as resolve5 } from "node:path";
1001
+ function assertFree(ctx, layer) {
1002
+ validateLayerName(layer);
1003
+ if (existsSync5(join3(ctx.sichDir, layer)))
1004
+ fail(`layer '${layer}' already exists (.sich/${layer}/)`);
1005
+ if (existsSync5(manifestPath(ctx, layer)))
1006
+ fail(`.sich/${layer}.paths already exists; remove it first`);
1007
+ }
1008
+ function createGitDir(ctx, layer) {
1009
+ const repo = layerRepo(ctx, layer);
1010
+ git(repo, ["init", "-q", "--initial-branch=main"], { cwd: ctx.root });
1011
+ git(repo, ["config", "core.worktree", "../.."]);
1012
+ git(repo, ["config", "core.bare", "false"]);
1013
+ return repo;
1014
+ }
1015
+ function remoteUrl(ctx, url) {
1016
+ if (isAbsolute2(url) || /^[a-z][a-z0-9+.-]*:\/\//i.test(url))
1017
+ return url;
1018
+ const local = resolve5(ctx.cwd, url);
1019
+ return existsSync5(local) ? local : url;
1020
+ }
1021
+ function baseRepoName(ctx) {
1022
+ const url = git(ctx.base, ["config", "--get", "remote.origin.url"], { allowFail: true }).stdout.trim();
1023
+ if (url) {
1024
+ const last = url.replace(/\/+$/, "").replace(/\.git$/, "").split(/[/:]/).pop();
1025
+ if (last)
1026
+ return last;
1027
+ }
1028
+ return basename3(ctx.root);
1029
+ }
1030
+ function runGh(args, capture) {
1031
+ const bin = process.env.SICH_GH || "gh";
1032
+ const r = spawnSync2(bin, args, {
1033
+ encoding: "utf8",
1034
+ stdio: ["inherit", capture ? "pipe" : "inherit", "inherit"]
1035
+ });
1036
+ if (r.error)
1037
+ fail(`cannot run ${bin}: ${r.error.message}`);
1038
+ if (r.status !== 0)
1039
+ fail(`${bin} ${args.join(" ")} failed (exit ${r.status ?? "?"})`);
1040
+ return r.stdout?.trim() ?? "";
1041
+ }
1042
+ function cmdNew(ctx, args) {
1043
+ const p = parseArgs(args, { "--remote": "value", "--gh": "optional" });
1044
+ const [layer, ...extra] = p.positionals;
1045
+ if (!layer || extra.length)
1046
+ fail("usage: sich new <layer> [--remote <url> | --gh [name]]");
1047
+ const given = str(p, "--remote");
1048
+ const remote = given && remoteUrl(ctx, given);
1049
+ const gh = p.flags["--gh"];
1050
+ if (remote && gh)
1051
+ fail("use either --remote or --gh, not both");
1052
+ assertFree(ctx, layer);
1053
+ ensureInit(ctx);
1054
+ let url = remote;
1055
+ if (gh) {
1056
+ const name = typeof gh === "string" ? gh : `${baseRepoName(ctx)}-${layer}`;
1057
+ runGh(["repo", "create", name, "--private"], false);
1058
+ url = runGh(["repo", "view", name, "--json", "sshUrl", "-q", ".sshUrl"], true);
1059
+ if (!url)
1060
+ fail(`could not read the SSH URL of GitHub repo ${name}`);
1061
+ }
1062
+ const repo = createGitDir(ctx, layer);
1063
+ try {
1064
+ writeFileSync4(manifestPath(ctx, layer), `# Paths owned by sich layer '${layer}': one root-relative path per line, directories end with /
1065
+ `);
1066
+ syncExcludes(ctx);
1067
+ git(repo, ["add", "-f", "--", manifestRel(layer)]);
1068
+ git(repo, ["commit", "-q", "-m", `sich: create layer ${layer}`]);
1069
+ if (url)
1070
+ git(repo, ["remote", "add", "origin", url]);
1071
+ } catch (e) {
1072
+ rmSync(repo.gitDir, { recursive: true, force: true });
1073
+ rmSync(manifestPath(ctx, layer), { force: true });
1074
+ syncExcludes(ctx);
1075
+ throw e;
1076
+ }
1077
+ out(`${c.green("created")} layer ${c.bold(layer)}: git data -> .sich/${layer}/, claims list -> ${manifestRel(layer)}`);
1078
+ if (url)
1079
+ out(`set origin of ${layer} -> ${url}`);
1080
+ note(`next: sich add ${layer} <path>... then sich commit ${layer} -m <msg>${url ? ` and sich push ${layer}` : ""}`);
1081
+ return 0;
1082
+ }
1083
+ function cmdAttach(ctx, args) {
1084
+ const p = parseArgs(args, {});
1085
+ const [layer, given, ...extra] = p.positionals;
1086
+ if (!layer || !given || extra.length)
1087
+ fail("usage: sich attach <layer> <url>");
1088
+ const url = remoteUrl(ctx, given);
1089
+ assertFree(ctx, layer);
1090
+ ensureInit(ctx);
1091
+ const repo = createGitDir(ctx, layer);
1092
+ const cleanup = () => rmSync(repo.gitDir, { recursive: true, force: true });
1093
+ try {
1094
+ git(repo, ["remote", "add", "origin", url]);
1095
+ const fetched = git(repo, ["fetch", "-q", "origin"], { allowFail: true });
1096
+ if (fetched.code !== 0)
1097
+ fail(`cannot fetch ${url}: ${fetched.stderr.trim().split(`
1098
+ `).pop()}`);
1099
+ const head = git(repo, ["ls-remote", "--symref", "origin", "HEAD"], { allowFail: true }).stdout;
1100
+ const branch = /^ref: refs\/heads\/(\S+)\s+HEAD$/m.exec(head)?.[1] ?? "main";
1101
+ const ref = `refs/remotes/origin/${branch}`;
1102
+ if (git(repo, ["rev-parse", "--verify", "-q", ref], { allowFail: true }).code !== 0) {
1103
+ fail(`${url} has no branch '${branch}' (is it a sich layer? create new layers with sich new)`);
1104
+ }
1105
+ const files = splitZ(git(repo, ["ls-tree", "-r", "-z", "--name-only", ref]).stdout);
1106
+ const clash = collisions(ctx, files);
1107
+ if (clash.length) {
1108
+ fail(`attach would overwrite existing files: ${listSome(clash)} (move them away and retry)`);
1109
+ }
1110
+ git(repo, ["checkout", "-q", "-B", branch, "--track", `origin/${branch}`]);
1111
+ if (!existsSync5(manifestPath(ctx, layer))) {
1112
+ warn(`${url} has no .sich/${layer}.paths; is the layer name right?`);
1113
+ }
1114
+ syncExcludes(ctx);
1115
+ out(`${c.green("attached")} layer ${c.bold(layer)} from ${url} -> .sich/${layer}/ (branch ${branch})`);
1116
+ out(`checked out ${plural(files.length, "file")}: ${listSome(files)}`);
1117
+ return 0;
1118
+ } catch (e) {
1119
+ cleanup();
1120
+ throw e;
1121
+ }
1122
+ }
1123
+
1124
+ // src/commands/status.ts
1125
+ function parseStatus(raw) {
1126
+ const s = {
1127
+ branch: "?",
1128
+ upstream: null,
1129
+ ahead: 0,
1130
+ behind: 0,
1131
+ gone: false,
1132
+ staged: 0,
1133
+ modified: 0,
1134
+ untracked: 0,
1135
+ conflicted: 0,
1136
+ files: []
1137
+ };
1138
+ const entries = splitZ(raw);
1139
+ let sawAb = false;
1140
+ for (let i = 0;i < entries.length; i++) {
1141
+ const e = entries[i];
1142
+ if (e.startsWith("# branch.head "))
1143
+ s.branch = e.slice(14);
1144
+ else if (e.startsWith("# branch.upstream "))
1145
+ s.upstream = e.slice(18);
1146
+ else if (e.startsWith("# branch.ab ")) {
1147
+ sawAb = true;
1148
+ const m = /\+(\d+) -(\d+)/.exec(e);
1149
+ s.ahead = Number(m?.[1] ?? 0);
1150
+ s.behind = Number(m?.[2] ?? 0);
1151
+ } else if (e.startsWith("? ")) {
1152
+ s.untracked++;
1153
+ s.files.push(`?? ${e.slice(2)}`);
1154
+ } else if (e[0] === "1" || e[0] === "2" || e[0] === "u") {
1155
+ const n = e[0] === "1" ? 8 : e[0] === "2" ? 9 : 10;
1156
+ const parts = e.split(" ");
1157
+ const xy = parts[1] ?? "..";
1158
+ const path = parts.slice(n).join(" ");
1159
+ if (e[0] === "2")
1160
+ i++;
1161
+ if (e[0] === "u")
1162
+ s.conflicted++;
1163
+ else {
1164
+ if (xy[0] !== ".")
1165
+ s.staged++;
1166
+ if (xy[1] !== ".")
1167
+ s.modified++;
1168
+ }
1169
+ s.files.push(`${xy.replace(/\./g, " ")} ${path}`);
1170
+ }
1171
+ }
1172
+ s.gone = s.upstream !== null && !sawAb;
1173
+ return s;
1174
+ }
1175
+ function repoStatus(repo) {
1176
+ return parseStatus(git(repo, ["status", "--porcelain=v2", "--branch", "-z"]).stdout);
1177
+ }
1178
+ function tracking(s) {
1179
+ if (!s.upstream)
1180
+ return c.dim("no upstream");
1181
+ if (s.gone)
1182
+ return `${s.upstream} ${c.red("gone")}`;
1183
+ const parts = [];
1184
+ if (s.ahead)
1185
+ parts.push(c.green(`ahead ${s.ahead}`));
1186
+ if (s.behind)
1187
+ parts.push(c.yellow(`behind ${s.behind}`));
1188
+ return `${s.upstream} ${parts.length ? parts.join(", ") : c.dim("up to date")}`;
1189
+ }
1190
+ function changes(s) {
1191
+ const parts = [];
1192
+ if (s.conflicted)
1193
+ parts.push(c.red(`${s.conflicted} conflicted`));
1194
+ if (s.staged)
1195
+ parts.push(c.green(`${s.staged} staged`));
1196
+ if (s.modified)
1197
+ parts.push(c.yellow(`${s.modified} modified`));
1198
+ if (s.untracked)
1199
+ parts.push(c.red(`${s.untracked} untracked`));
1200
+ return parts.length ? parts.join(", ") : c.dim("clean");
1201
+ }
1202
+ var visible = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
1203
+ var pad = (s, w) => s + " ".repeat(Math.max(0, w - visible(s)));
1204
+ function cmdStatus(ctx, args) {
1205
+ const p = parseArgs(args, { "-v": "bool", "--fetch": "bool" }, { "--verbose": "-v" });
1206
+ if (p.positionals.length)
1207
+ fail("usage: sich status [-v] [--fetch]");
1208
+ const repos = [ctx.base, ...layerNames(ctx).map((l) => layerRepo(ctx, l))];
1209
+ if (p.flags["--fetch"]) {
1210
+ for (const r of repos) {
1211
+ if (remotesOf(r).length === 0)
1212
+ continue;
1213
+ const f = git(r, ["fetch", "--quiet"], { allowFail: true });
1214
+ if (f.code !== 0)
1215
+ warn(`${r.name}: fetch failed: ${f.stderr.trim().split(`
1216
+ `).pop()}`);
1217
+ }
1218
+ }
1219
+ const rows = repos.map((r) => ({ repo: r, s: repoStatus(r) }));
1220
+ const rendered = rows.map(({ repo, s }) => [c.bold(repo.name), c.cyan(s.branch), tracking(s), changes(s)]);
1221
+ const widths = [0, 1, 2].map((i) => Math.max(...rendered.map((cols) => visible(cols[i]))));
1222
+ rows.forEach(({ s }, idx) => {
1223
+ const cols = rendered[idx];
1224
+ out(cols.map((col, i) => i < 3 ? pad(col, widths[i]) : col).join(" "));
1225
+ if (p.flags["-v"])
1226
+ for (const f of s.files)
1227
+ out(` ${f}`);
1228
+ });
1229
+ return 0;
1230
+ }
1231
+
1232
+ // src/commands/sync.ts
1233
+ class StepFailed extends Error {
1234
+ repo;
1235
+ action;
1236
+ reason;
1237
+ constructor(repo, action, reason) {
1238
+ super(`${action} failed in ${repo}`);
1239
+ this.repo = repo;
1240
+ this.action = action;
1241
+ this.reason = reason;
1242
+ }
1243
+ }
1244
+ function header(repo, argv) {
1245
+ out(`${c.bold(repo.name)}: ${c.dim("git " + argv.join(" "))}`);
1246
+ }
1247
+ function exec(repo, action, argv) {
1248
+ const code = gitInherit(repo, argv);
1249
+ if (code !== 0)
1250
+ throw new StepFailed(repo.name, action, `git exit ${code}`);
1251
+ }
1252
+ function defaultRemote(remotes) {
1253
+ return remotes.includes("origin") ? "origin" : remotes[0];
1254
+ }
1255
+ function refuseOverwrite(ctx, repo, incoming) {
1256
+ const hasHead = git(repo, ["rev-parse", "--verify", "-q", "HEAD"], { allowFail: true }).code === 0;
1257
+ const added = hasHead ? git(repo, ["diff", "--name-only", "-z", "--no-renames", "--diff-filter=A", "HEAD", incoming]).stdout : git(repo, ["ls-tree", "-r", "-z", "--name-only", incoming]).stdout;
1258
+ const clash = collisions(ctx, splitZ(added)).filter((p) => trackedUnder(repo, [p]).length === 0);
1259
+ if (clash.length === 0)
1260
+ return;
1261
+ const claims = allClaims(ctx);
1262
+ const shown = clash.map((p) => {
1263
+ const owner = ownerOf(claims, p)?.layer;
1264
+ return owner && owner !== repo.name ? `${p} (${owner})` : p;
1265
+ });
1266
+ printError(`${repo.name}: pull would overwrite ${plural(clash.length, "local path")} it doesn't track: ` + `${listSome(shown)} (move aside and retry)`);
1267
+ throw new StepFailed(repo.name, "pull", "would overwrite local files");
1268
+ }
1269
+ function pull(ctx, repo, remotes) {
1270
+ const argv = ["pull", "--rebase", "--autostash"];
1271
+ let fetch;
1272
+ let incoming;
1273
+ if (upstreamOf(repo)) {
1274
+ fetch = [];
1275
+ incoming = "@{u}";
1276
+ } else {
1277
+ const branch = currentBranch(repo);
1278
+ if (!branch) {
1279
+ note(`${repo.name}: detached HEAD and no upstream, skipped pull`);
1280
+ return;
1281
+ }
1282
+ const remote = defaultRemote(remotes);
1283
+ const has = git(repo, ["ls-remote", "--exit-code", "--heads", remote, branch], { allowFail: true }).code === 0;
1284
+ if (!has) {
1285
+ note(`${repo.name}: no upstream and nothing on ${remote} yet, skipped pull`);
1286
+ return;
1287
+ }
1288
+ fetch = [remote, branch];
1289
+ incoming = "FETCH_HEAD";
1290
+ argv.push(remote, branch);
1291
+ }
1292
+ header(repo, argv);
1293
+ exec(repo, "pull", ["fetch", "-q", ...fetch]);
1294
+ refuseOverwrite(ctx, repo, incoming);
1295
+ exec(repo, "pull", argv);
1296
+ syncExcludes(ctx);
1297
+ }
1298
+ function push(repo, remotes) {
1299
+ if (upstreamOf(repo)) {
1300
+ header(repo, ["push"]);
1301
+ exec(repo, "push", ["push"]);
1302
+ return;
1303
+ }
1304
+ const branch = currentBranch(repo);
1305
+ if (!branch) {
1306
+ printError(`${repo.name}: detached HEAD, cannot push`);
1307
+ throw new StepFailed(repo.name, "push", "detached HEAD");
1308
+ }
1309
+ const argv = ["push", "-u", defaultRemote(remotes), branch];
1310
+ header(repo, argv);
1311
+ exec(repo, "push", argv);
1312
+ }
1313
+ function cmdRemote(ctx, command, args) {
1314
+ const p = parseArgs(args, {});
1315
+ const names = p.positionals.length ? [...new Set(p.positionals)] : ["base", ...layerNames(ctx)];
1316
+ const repos = names.map((n) => repoByName(ctx, n));
1317
+ const actions = command === "sync" ? ["pull", "push"] : [command];
1318
+ for (let i = 0;i < repos.length; i++) {
1319
+ const repo = repos[i];
1320
+ const remotes = remotesOf(repo);
1321
+ if (remotes.length === 0) {
1322
+ note(`${repo.name}: no remote, skipped`);
1323
+ continue;
1324
+ }
1325
+ try {
1326
+ for (const a of actions) {
1327
+ if (a === "pull")
1328
+ pull(ctx, repo, remotes);
1329
+ else
1330
+ push(repo, remotes);
1331
+ }
1332
+ } catch (e) {
1333
+ if (!(e instanceof StepFailed))
1334
+ throw e;
1335
+ syncExcludes(ctx);
1336
+ const rest = repos.slice(i + 1).map((r) => r.name);
1337
+ fail(`${e.action} failed in ${e.repo} (${e.reason})` + (rest.length ? `; stopped before: ${rest.join(", ")}` : ""));
1338
+ }
1339
+ }
1340
+ return 0;
1341
+ }
1342
+
1343
+ // src/help.ts
1344
+ var MAIN_HELP = `sich - private git layers that share one working tree with a normal repo
1345
+
1346
+ base your normal repo (.git), usually public
1347
+ layer a private repo in .sich/<layer>/ with its own remote and access list
1348
+ claim a file or folder a layer owns; base and other layers never see it
1349
+
1350
+ usage: sich [-C <dir>] <command> [args]
1351
+ sich [-C <dir>] <layer|base> <git args...>
1352
+
1353
+ setup
1354
+ init set up .sich/, base excludes, commit guard
1355
+ new <layer> [--remote <url> | --gh [name]]
1356
+ create a layer (--gh: private GitHub repo)
1357
+ attach <layer> <url> join an existing layer (collaborators)
1358
+
1359
+ ownership
1360
+ add <layer> <path...> [--move] claim files/folders for a layer, stage them
1361
+ (--move: take them from base/another layer)
1362
+ rm <layer> <path...> release claims; files stay on disk
1363
+ which <path> show who owns a path
1364
+ ls [layer] list claims and tracked files
1365
+
1366
+ everyday
1367
+ status [-v] [--fetch] per repo: branch, ahead/behind, changes
1368
+ commit [repo...] -m <msg> add -A + commit base + all layers (or named)
1369
+ pull [repo...] pull --rebase base + all layers (or named)
1370
+ push [repo...] push base + all layers (or named)
1371
+ sync [repo...] pull, then push
1372
+ check [--fix] [--staged] find leaks and ownership problems
1373
+ (--staged: what the pre-commit hook runs)
1374
+
1375
+ passthrough
1376
+ <layer> <git args...> run git in a layer, e.g. sich notes log
1377
+ base <git args...> run git in the base repo
1378
+
1379
+ options
1380
+ -C <dir> run as if started in <dir>
1381
+ -h, --help show help (also: sich <command> --help)
1382
+ -V, --version print version
1383
+
1384
+ environment
1385
+ SICH_BIN sich binary the pre-commit hook runs
1386
+ (default: sich on PATH)
1387
+ SICH_GH gh binary for new --gh (default: gh)
1388
+ NO_COLOR disable colors
1389
+
1390
+ Layer names match ^[a-z0-9][a-z0-9._-]*$; base and command names are reserved.
1391
+ Claims can't nest across layers.
1392
+ `;
1393
+ var COMMAND_HELP = {
1394
+ init: `usage: sich init
1395
+
1396
+ Creates .sich/, writes the managed block in the base repo's info/exclude, and
1397
+ installs a pre-commit hook that runs 'sich check --staged'. If a pre-commit hook
1398
+ already exists or core.hooksPath is set, prints the line to add instead.
1399
+ The hook runs $SICH_BIN (default: sich on PATH) and blocks the commit if it
1400
+ can't be found. Safe to run again.`,
1401
+ new: `usage: sich new <layer> [--remote <url> | --gh [name]]
1402
+
1403
+ Creates layer <layer> (.sich/<layer>/ git dir sharing this working tree) with an
1404
+ empty claims manifest .sich/<layer>.paths and an initial commit.
1405
+ --remote <url> set origin
1406
+ --gh [name] create a private GitHub repo with gh (default name:
1407
+ <base-repo>-<layer>) and set origin to its SSH URL.
1408
+ The gh binary can be overridden with SICH_GH.`,
1409
+ attach: `usage: sich attach <layer> <url>
1410
+
1411
+ Joins an existing layer: fetches <url> and checks out its default branch into
1412
+ this working tree. Refuses to overwrite existing files.`,
1413
+ add: `usage: sich add <layer> <path...> [--move]
1414
+
1415
+ Claims paths (files or directories, relative to the current directory) for a
1416
+ layer, then stages them and the manifest in that layer (no commit).
1417
+ Refuses paths tracked by base or owned by another layer unless --move is given;
1418
+ --move untracks them from the previous owner (old versions stay in its history).
1419
+ Claims can't nest across layers: a path inside another layer's claimed directory
1420
+ is always refused (move the whole directory instead).`,
1421
+ rm: `usage: sich rm <layer> <path...>
1422
+
1423
+ Removes exact claims from a layer and untracks them there. The files stay on
1424
+ disk and show up as untracked in base (or, if another layer's directory claim
1425
+ contains them, now belong to that layer).`,
1426
+ which: `usage: sich which <path>
1427
+
1428
+ Prints the owner of a path: a layer name, 'base', 'ignored' or 'untracked'.`,
1429
+ ls: `usage: sich ls [layer]
1430
+
1431
+ Lists each layer's claims and tracked files.`,
1432
+ status: `usage: sich status [-v] [--fetch]
1433
+
1434
+ One row per repo (base first): branch, ahead/behind upstream, and counts of
1435
+ staged / modified / untracked files.
1436
+ -v, --verbose list the files
1437
+ --fetch fetch every repo with a remote first`,
1438
+ commit: `usage: sich commit [repo...] -m <msg>
1439
+
1440
+ Stages everything (git add -A) and commits, in base and every layer with
1441
+ changes, or only in the named repos ('base' or layer names). Excludes keep each
1442
+ repo to its own files; anything a repo doesn't own (e.g. let in by a '!' rule in
1443
+ a .gitignore) is left out with a warning. Base commits run its pre-commit hook.`,
1444
+ pull: `usage: sich pull [repo...]
1445
+
1446
+ git pull --rebase --autostash in base and every layer (or the named repos,
1447
+ 'base' allowed). Repos without a remote are skipped. Stops at the first failure.
1448
+ Refuses to pull a repo if that would overwrite a file it doesn't track, such as
1449
+ another layer's (git would do that silently: to it, those files are ignored).`,
1450
+ push: `usage: sich push [repo...]
1451
+
1452
+ git push in base and every layer (or the named repos). Sets the upstream
1453
+ (-u origin <branch>) when there is none. Stops at the first failure.`,
1454
+ sync: `usage: sich sync [repo...]
1455
+
1456
+ pull then push, repo by repo. Stops at the first failure.`,
1457
+ check: `usage: sich check [--fix] [--staged]
1458
+
1459
+ Reports: paths claimed or tracked by two layers, base tracking claimed paths or
1460
+ .sich/, layers tracking files they don't own, and stale exclude blocks.
1461
+ --fix rewrite stale exclude blocks
1462
+ --staged also fail if base's index stages claimed paths or .sich/
1463
+ (this is what the pre-commit hook runs; silent when all is well,
1464
+ stale excludes only warn, and a no-op where sich isn't set up,
1465
+ e.g. a linked worktree)
1466
+ Exits 1 if any issue remains.`,
1467
+ help: `usage: sich help [command]`
1468
+ };
1469
+
1470
+ // src/cli.ts
1471
+ var VERSION = version;
1472
+ var HANDLERS = {
1473
+ init: cmdInit,
1474
+ new: cmdNew,
1475
+ attach: cmdAttach,
1476
+ add: cmdAdd,
1477
+ rm: cmdRm,
1478
+ which: cmdWhich,
1479
+ ls: cmdLs,
1480
+ status: cmdStatus,
1481
+ commit: cmdCommit,
1482
+ pull: (ctx, a) => cmdRemote(ctx, "pull", a),
1483
+ push: (ctx, a) => cmdRemote(ctx, "push", a),
1484
+ sync: (ctx, a) => cmdRemote(ctx, "sync", a),
1485
+ check: cmdCheck
1486
+ };
1487
+ var SELF_INIT = new Set(["init", "new", "attach", "check"]);
1488
+ function changeDir(cwd, dir) {
1489
+ const target = resolve6(cwd, dir);
1490
+ try {
1491
+ if (!statSync(target).isDirectory())
1492
+ fail(`-C ${dir}: not a directory`);
1493
+ return realpathSync3.native(target);
1494
+ } catch (e) {
1495
+ if (e instanceof SichError)
1496
+ throw e;
1497
+ fail(`-C ${dir}: no such directory`);
1498
+ }
1499
+ }
1500
+ function main(argv) {
1501
+ let cwd = realpathSync3.native(process.cwd());
1502
+ let i = 0;
1503
+ for (;i < argv.length; i++) {
1504
+ const a = argv[i];
1505
+ if (a === "-C") {
1506
+ const dir = argv[++i];
1507
+ if (dir === undefined)
1508
+ fail("-C needs a directory");
1509
+ cwd = changeDir(cwd, dir);
1510
+ } else if (a.startsWith("-C") && a.length > 2) {
1511
+ cwd = changeDir(cwd, a.slice(2));
1512
+ } else if (a === "-h" || a === "--help") {
1513
+ out(MAIN_HELP.trimEnd());
1514
+ return 0;
1515
+ } else if (a === "-V" || a === "--version") {
1516
+ out(`sich ${VERSION}`);
1517
+ return 0;
1518
+ } else if (a.startsWith("-")) {
1519
+ fail(`unknown option '${a}' (see sich --help)`);
1520
+ } else
1521
+ break;
1522
+ }
1523
+ const name = argv[i];
1524
+ const rest = argv.slice(i + 1);
1525
+ if (name === undefined) {
1526
+ out(MAIN_HELP.trimEnd());
1527
+ return 0;
1528
+ }
1529
+ if (name === "help") {
1530
+ const topic = rest[0];
1531
+ if (topic && !COMMAND_HELP[topic])
1532
+ fail(`no help for '${topic}'`);
1533
+ out((topic ? COMMAND_HELP[topic] : MAIN_HELP).trimEnd());
1534
+ return 0;
1535
+ }
1536
+ if (Object.hasOwn(HANDLERS, name)) {
1537
+ if (wantsHelp(rest)) {
1538
+ out(COMMAND_HELP[name]);
1539
+ return 0;
1540
+ }
1541
+ const ctx2 = loadCtx(cwd);
1542
+ if (!SELF_INIT.has(name))
1543
+ requireInit(ctx2);
1544
+ if (name !== "check")
1545
+ syncExcludes(ctx2);
1546
+ return HANDLERS[name](ctx2, rest);
1547
+ }
1548
+ const ctx = loadCtx(cwd);
1549
+ if (name !== "base" && !layerExists(ctx, name)) {
1550
+ fail(`unknown command or layer '${name}' (see sich --help)`);
1551
+ }
1552
+ syncExcludes(ctx);
1553
+ const code = gitInherit(repoByName(ctx, name), rest, ctx.cwd);
1554
+ syncExcludes(ctx);
1555
+ return code;
1556
+ }
1557
+ var code;
1558
+ try {
1559
+ code = main(process.argv.slice(2));
1560
+ } catch (e) {
1561
+ if (e instanceof SichError)
1562
+ printError(e.message);
1563
+ else
1564
+ printError(process.env.SICH_DEBUG ? String(e.stack) : String(e.message ?? e));
1565
+ code = 1;
1566
+ }
1567
+ process.exit(code);
1568
+ export {
1569
+ main
1570
+ };