nocaap 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2491 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { confirm, input, checkbox } from '@inquirer/prompts';
4
+ import upath from 'upath';
5
+ import 'path';
6
+ import fs2 from 'fs-extra';
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+ import { z } from 'zod';
10
+ import os from 'os';
11
+ import simpleGit from 'simple-git';
12
+ import matter from 'gray-matter';
13
+ import { exec } from 'child_process';
14
+ import { promisify } from 'util';
15
+
16
+ var CONTEXT_DIR = ".context";
17
+ var PACKAGES_DIR = "packages";
18
+ var CONFIG_FILE = "context.config.json";
19
+ var LOCK_FILE = "context.lock";
20
+ var INDEX_FILE = "INDEX.md";
21
+ function toUnix(filePath) {
22
+ return upath.toUnix(filePath);
23
+ }
24
+ function join(...segments) {
25
+ return upath.join(...segments);
26
+ }
27
+ function dirname(filePath) {
28
+ return upath.dirname(filePath);
29
+ }
30
+ function basename(filePath, ext) {
31
+ return upath.basename(filePath, ext);
32
+ }
33
+ function extname(filePath) {
34
+ return upath.extname(filePath);
35
+ }
36
+ function getContextDir(projectRoot) {
37
+ return join(projectRoot, CONTEXT_DIR);
38
+ }
39
+ function getPackagesDir(projectRoot) {
40
+ return join(getContextDir(projectRoot), PACKAGES_DIR);
41
+ }
42
+ function getConfigPath(projectRoot) {
43
+ return join(getContextDir(projectRoot), CONFIG_FILE);
44
+ }
45
+ function getLockfilePath(projectRoot) {
46
+ return join(getContextDir(projectRoot), LOCK_FILE);
47
+ }
48
+ function getIndexPath(projectRoot) {
49
+ return join(getContextDir(projectRoot), INDEX_FILE);
50
+ }
51
+ function getPackagePath(projectRoot, alias) {
52
+ return join(getPackagesDir(projectRoot), alias);
53
+ }
54
+ function relative(from, to) {
55
+ return upath.relative(from, to);
56
+ }
57
+ async function ensureDir(dirPath) {
58
+ await fs2.ensureDir(dirPath);
59
+ }
60
+ async function exists(filePath) {
61
+ try {
62
+ await fs2.access(filePath);
63
+ return true;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+ var log = {
69
+ /** Info message (blue) */
70
+ info: (message) => console.log(chalk.blue("\u2139"), message),
71
+ /** Success message (green) */
72
+ success: (message) => console.log(chalk.green("\u2714"), message),
73
+ /** Warning message (yellow) */
74
+ warn: (message) => console.log(chalk.yellow("\u26A0"), message),
75
+ /** Error message (red) */
76
+ error: (message) => console.log(chalk.red("\u2716"), message),
77
+ /** Debug message (gray) - only when NOCAAP_DEBUG=true */
78
+ debug: (message) => {
79
+ if (process.env.NOCAAP_DEBUG === "true") {
80
+ console.log(chalk.gray("\u{1F50D}"), chalk.gray(message));
81
+ }
82
+ },
83
+ /** Plain message */
84
+ plain: (message) => console.log(message),
85
+ /** Styled title */
86
+ title: (message) => console.log(chalk.bold.cyan(`
87
+ ${message}
88
+ `)),
89
+ /** Dim helper text */
90
+ dim: (message) => console.log(chalk.dim(message)),
91
+ /** Empty line for spacing */
92
+ newline: () => console.log(),
93
+ /** Horizontal rule */
94
+ hr: () => console.log(chalk.dim("\u2500".repeat(50)))
95
+ };
96
+ var style = {
97
+ bold: (text) => chalk.bold(text),
98
+ dim: (text) => chalk.dim(text),
99
+ italic: (text) => chalk.italic(text),
100
+ underline: (text) => chalk.underline(text),
101
+ code: (text) => chalk.cyan(`\`${text}\``),
102
+ path: (text) => chalk.yellow(text),
103
+ url: (text) => chalk.blue.underline(text),
104
+ success: (text) => chalk.green(text),
105
+ error: (text) => chalk.red(text),
106
+ warn: (text) => chalk.yellow(text)
107
+ };
108
+ function createSpinner(text) {
109
+ const spinner = ora({
110
+ text,
111
+ color: "cyan"
112
+ });
113
+ const instance = {
114
+ start(newText) {
115
+ spinner.start(newText);
116
+ return instance;
117
+ },
118
+ stop() {
119
+ spinner.stop();
120
+ return instance;
121
+ },
122
+ succeed(newText) {
123
+ spinner.succeed(newText);
124
+ return instance;
125
+ },
126
+ fail(newText) {
127
+ spinner.fail(newText);
128
+ return instance;
129
+ },
130
+ warn(newText) {
131
+ spinner.warn(newText);
132
+ return instance;
133
+ },
134
+ info(newText) {
135
+ spinner.info(newText);
136
+ return instance;
137
+ },
138
+ get text() {
139
+ return spinner.text;
140
+ },
141
+ set text(value) {
142
+ spinner.text = value;
143
+ }
144
+ };
145
+ return instance;
146
+ }
147
+ var gitUrlPattern = /^(git@|https:\/\/|git:\/\/).+/;
148
+ var ContextEntrySchema = z.object({
149
+ name: z.string().min(1, "Context name is required"),
150
+ description: z.string(),
151
+ repo: z.string().min(1, "Repository URL is required").refine(
152
+ (url) => gitUrlPattern.test(url),
153
+ "Must be a valid Git URL (git@, https://, or git://)"
154
+ ),
155
+ path: z.string().optional(),
156
+ tags: z.array(z.string()).optional()
157
+ });
158
+ var RegistrySchema = z.object({
159
+ name: z.string().optional(),
160
+ contexts: z.array(ContextEntrySchema),
161
+ imports: z.array(z.string().url()).optional()
162
+ });
163
+ var PackageEntrySchema = z.object({
164
+ alias: z.string().min(1, "Alias is required"),
165
+ source: z.string().min(1, "Source URL is required"),
166
+ path: z.string().optional(),
167
+ version: z.string().default("main")
168
+ });
169
+ var ConfigSchema = z.object({
170
+ registryUrl: z.string().url().optional(),
171
+ packages: z.array(PackageEntrySchema)
172
+ });
173
+ var LockEntrySchema = z.object({
174
+ commitHash: z.string().min(1, "Commit hash is required"),
175
+ sparsePath: z.string(),
176
+ updatedAt: z.string().datetime()
177
+ });
178
+ var LockfileSchema = z.record(z.string(), LockEntrySchema);
179
+ function safeValidate(schema, data) {
180
+ const result = schema.safeParse(data);
181
+ return result.success ? { success: true, data: result.data } : { success: false, error: result.error };
182
+ }
183
+ function safeValidateRegistry(data) {
184
+ return safeValidate(RegistrySchema, data);
185
+ }
186
+ function safeValidateConfig(data) {
187
+ const result = ConfigSchema.safeParse(data);
188
+ return result.success ? { success: true, data: result.data } : { success: false, error: result.error };
189
+ }
190
+ function safeValidateLockfile(data) {
191
+ return safeValidate(LockfileSchema, data);
192
+ }
193
+
194
+ // src/core/config.ts
195
+ async function initContextDir(projectRoot) {
196
+ const contextDir = getContextDir(projectRoot);
197
+ const packagesDir = getPackagesDir(projectRoot);
198
+ const configPath = getConfigPath(projectRoot);
199
+ const lockfilePath = getLockfilePath(projectRoot);
200
+ log.debug(`Initializing context directory at ${contextDir}`);
201
+ await ensureDir(contextDir);
202
+ await ensureDir(packagesDir);
203
+ if (!await exists(configPath)) {
204
+ const defaultConfig = { packages: [] };
205
+ await fs2.writeJson(configPath, defaultConfig, { spaces: 2 });
206
+ log.debug(`Created default config at ${configPath}`);
207
+ }
208
+ if (!await exists(lockfilePath)) {
209
+ const defaultLockfile = {};
210
+ await fs2.writeJson(lockfilePath, defaultLockfile, { spaces: 2 });
211
+ log.debug(`Created default lockfile at ${lockfilePath}`);
212
+ }
213
+ await updateGitignore(projectRoot);
214
+ }
215
+ async function configExists(projectRoot) {
216
+ const configPath = getConfigPath(projectRoot);
217
+ return exists(configPath);
218
+ }
219
+ async function readConfig(projectRoot) {
220
+ const configPath = getConfigPath(projectRoot);
221
+ if (!await exists(configPath)) {
222
+ log.debug(`Config file not found at ${configPath}`);
223
+ return null;
224
+ }
225
+ try {
226
+ const data = await fs2.readJson(configPath);
227
+ const result = safeValidateConfig(data);
228
+ if (!result.success) {
229
+ const errorMessages = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
230
+ throw new Error(`Invalid config file: ${errorMessages}`);
231
+ }
232
+ log.debug(`Read config from ${configPath}`);
233
+ return result.data;
234
+ } catch (error) {
235
+ if (error instanceof SyntaxError) {
236
+ throw new Error(`Failed to parse config file: Invalid JSON at ${configPath}`);
237
+ }
238
+ throw error;
239
+ }
240
+ }
241
+ async function writeConfig(projectRoot, config) {
242
+ const configPath = getConfigPath(projectRoot);
243
+ const result = safeValidateConfig(config);
244
+ if (!result.success) {
245
+ const errorMessages = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
246
+ throw new Error(`Invalid config data: ${errorMessages}`);
247
+ }
248
+ await fs2.writeJson(configPath, config, { spaces: 2 });
249
+ log.debug(`Wrote config to ${configPath}`);
250
+ }
251
+ async function readLockfile(projectRoot) {
252
+ const lockfilePath = getLockfilePath(projectRoot);
253
+ if (!await exists(lockfilePath)) {
254
+ log.debug(`Lockfile not found at ${lockfilePath}, returning empty lockfile`);
255
+ return {};
256
+ }
257
+ try {
258
+ const data = await fs2.readJson(lockfilePath);
259
+ const result = safeValidateLockfile(data);
260
+ if (!result.success) {
261
+ const errorMessages = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
262
+ throw new Error(`Invalid lockfile: ${errorMessages}`);
263
+ }
264
+ log.debug(`Read lockfile from ${lockfilePath}`);
265
+ return result.data;
266
+ } catch (error) {
267
+ if (error instanceof SyntaxError) {
268
+ throw new Error(`Failed to parse lockfile: Invalid JSON at ${lockfilePath}`);
269
+ }
270
+ throw error;
271
+ }
272
+ }
273
+ async function writeLockfile(projectRoot, lockfile) {
274
+ const lockfilePath = getLockfilePath(projectRoot);
275
+ const result = safeValidateLockfile(lockfile);
276
+ if (!result.success) {
277
+ const errorMessages = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
278
+ throw new Error(`Invalid lockfile data: ${errorMessages}`);
279
+ }
280
+ await fs2.writeJson(lockfilePath, lockfile, { spaces: 2 });
281
+ log.debug(`Wrote lockfile to ${lockfilePath}`);
282
+ }
283
+ async function updateLockEntry(projectRoot, alias, entry) {
284
+ const lockfile = await readLockfile(projectRoot);
285
+ lockfile[alias] = {
286
+ ...entry,
287
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
288
+ };
289
+ await writeLockfile(projectRoot, lockfile);
290
+ log.debug(`Updated lock entry for alias '${alias}'`);
291
+ }
292
+ async function removeLockEntry(projectRoot, alias) {
293
+ const lockfile = await readLockfile(projectRoot);
294
+ if (alias in lockfile) {
295
+ delete lockfile[alias];
296
+ await writeLockfile(projectRoot, lockfile);
297
+ log.debug(`Removed lock entry for alias '${alias}'`);
298
+ }
299
+ }
300
+ async function getLockEntry(projectRoot, alias) {
301
+ const lockfile = await readLockfile(projectRoot);
302
+ return lockfile[alias];
303
+ }
304
+ async function upsertPackage(projectRoot, pkg) {
305
+ const config = await readConfig(projectRoot) ?? { packages: [] };
306
+ const existingIndex = config.packages.findIndex((p) => p.alias === pkg.alias);
307
+ if (existingIndex >= 0) {
308
+ config.packages[existingIndex] = pkg;
309
+ log.debug(`Updated package '${pkg.alias}' in config`);
310
+ } else {
311
+ config.packages.push(pkg);
312
+ log.debug(`Added package '${pkg.alias}' to config`);
313
+ }
314
+ await writeConfig(projectRoot, config);
315
+ }
316
+ async function removePackage(projectRoot, alias) {
317
+ const config = await readConfig(projectRoot);
318
+ if (!config) return false;
319
+ const initialLength = config.packages.length;
320
+ config.packages = config.packages.filter((p) => p.alias !== alias);
321
+ if (config.packages.length < initialLength) {
322
+ await writeConfig(projectRoot, config);
323
+ log.debug(`Removed package '${alias}' from config`);
324
+ return true;
325
+ }
326
+ return false;
327
+ }
328
+ async function getPackage(projectRoot, alias) {
329
+ const config = await readConfig(projectRoot);
330
+ return config?.packages.find((p) => p.alias === alias);
331
+ }
332
+ var GITIGNORE_ENTRY = ".context/packages/";
333
+ var GITIGNORE_COMMENT = "# nocaap packages (auto-generated)";
334
+ async function updateGitignore(projectRoot) {
335
+ const gitignorePath = join(projectRoot, ".gitignore");
336
+ try {
337
+ if (await exists(gitignorePath)) {
338
+ const content = await fs2.readFile(gitignorePath, "utf-8");
339
+ if (content.includes(GITIGNORE_ENTRY)) {
340
+ log.debug(".gitignore already contains nocaap entry");
341
+ return false;
342
+ }
343
+ const newContent = content.endsWith("\n") ? content : content + "\n";
344
+ await fs2.writeFile(gitignorePath, `${newContent}
345
+ ${GITIGNORE_COMMENT}
346
+ ${GITIGNORE_ENTRY}
347
+ `);
348
+ } else {
349
+ await fs2.writeFile(gitignorePath, `${GITIGNORE_COMMENT}
350
+ ${GITIGNORE_ENTRY}
351
+ `);
352
+ }
353
+ log.debug("Updated .gitignore with nocaap entry");
354
+ return true;
355
+ } catch (error) {
356
+ log.debug(`Failed to update .gitignore: ${error}`);
357
+ return false;
358
+ }
359
+ }
360
+ var CURSOR_RULES_CONTENT = `# nocaap Context
361
+ This project uses nocaap for organizational context.
362
+ Read .context/INDEX.md for available documentation.
363
+ `;
364
+ async function updateCursorRules(projectRoot) {
365
+ const cursorDir = join(projectRoot, ".cursor");
366
+ const cursorRulesPath = join(cursorDir, "rules");
367
+ const legacyCursorRulesPath = join(projectRoot, ".cursorrules");
368
+ try {
369
+ for (const rulePath of [cursorRulesPath, legacyCursorRulesPath]) {
370
+ if (await exists(rulePath)) {
371
+ const content = await fs2.readFile(rulePath, "utf-8");
372
+ if (content.includes(".context/INDEX.md")) {
373
+ log.debug("Cursor rules already contain nocaap reference");
374
+ return false;
375
+ }
376
+ }
377
+ }
378
+ if (await exists(cursorDir)) {
379
+ if (await exists(cursorRulesPath)) {
380
+ const content = await fs2.readFile(cursorRulesPath, "utf-8");
381
+ const newContent = content.endsWith("\n") ? content : content + "\n";
382
+ await fs2.writeFile(cursorRulesPath, `${newContent}
383
+ ${CURSOR_RULES_CONTENT}`);
384
+ } else {
385
+ await fs2.writeFile(cursorRulesPath, CURSOR_RULES_CONTENT);
386
+ }
387
+ log.debug("Updated .cursor/rules with nocaap reference");
388
+ return true;
389
+ }
390
+ if (await exists(legacyCursorRulesPath)) {
391
+ const content = await fs2.readFile(legacyCursorRulesPath, "utf-8");
392
+ const newContent = content.endsWith("\n") ? content : content + "\n";
393
+ await fs2.writeFile(legacyCursorRulesPath, `${newContent}
394
+ ${CURSOR_RULES_CONTENT}`);
395
+ } else {
396
+ await fs2.writeFile(legacyCursorRulesPath, CURSOR_RULES_CONTENT);
397
+ }
398
+ log.debug("Updated .cursorrules with nocaap reference");
399
+ return true;
400
+ } catch (error) {
401
+ log.debug(`Failed to update Cursor rules: ${error}`);
402
+ return false;
403
+ }
404
+ }
405
+ var CLAUDE_MD_CONTENT = `
406
+ ## Project Context
407
+ This project uses nocaap for organizational context.
408
+ Read \`.context/INDEX.md\` for standards, guidelines, and documentation.
409
+ `;
410
+ async function updateClaudeMd(projectRoot) {
411
+ const claudeMdPath = join(projectRoot, "CLAUDE.md");
412
+ try {
413
+ if (await exists(claudeMdPath)) {
414
+ const content = await fs2.readFile(claudeMdPath, "utf-8");
415
+ if (content.includes(".context/INDEX.md")) {
416
+ log.debug("CLAUDE.md already contains nocaap reference");
417
+ return false;
418
+ }
419
+ const newContent = content.endsWith("\n") ? content : content + "\n";
420
+ await fs2.writeFile(claudeMdPath, `${newContent}${CLAUDE_MD_CONTENT}`);
421
+ } else {
422
+ await fs2.writeFile(claudeMdPath, `# CLAUDE.md${CLAUDE_MD_CONTENT}`);
423
+ }
424
+ log.debug("Updated CLAUDE.md with nocaap reference");
425
+ return true;
426
+ } catch (error) {
427
+ log.debug(`Failed to update CLAUDE.md: ${error}`);
428
+ return false;
429
+ }
430
+ }
431
+ var NOCAAP_DIR = ".nocaap";
432
+ var CONFIG_FILE2 = "config.json";
433
+ function getGlobalConfigDir() {
434
+ return join(os.homedir(), NOCAAP_DIR);
435
+ }
436
+ function getGlobalConfigPath() {
437
+ return join(getGlobalConfigDir(), CONFIG_FILE2);
438
+ }
439
+ async function getGlobalConfig() {
440
+ const configPath = getGlobalConfigPath();
441
+ if (!await exists(configPath)) {
442
+ log.debug(`Global config not found at ${configPath}`);
443
+ return {};
444
+ }
445
+ try {
446
+ const data = await fs2.readJson(configPath);
447
+ log.debug(`Read global config from ${configPath}`);
448
+ return data;
449
+ } catch (error) {
450
+ log.debug(`Failed to read global config: ${error}`);
451
+ return {};
452
+ }
453
+ }
454
+ async function setGlobalConfig(config) {
455
+ const configDir = getGlobalConfigDir();
456
+ const configPath = getGlobalConfigPath();
457
+ await fs2.ensureDir(configDir);
458
+ config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
459
+ await fs2.writeJson(configPath, config, { spaces: 2 });
460
+ log.debug(`Wrote global config to ${configPath}`);
461
+ }
462
+ async function getDefaultRegistry() {
463
+ const envRegistry = process.env.NOCAAP_DEFAULT_REGISTRY;
464
+ if (envRegistry) {
465
+ log.debug(`Using registry from NOCAAP_DEFAULT_REGISTRY env var`);
466
+ return envRegistry;
467
+ }
468
+ const config = await getGlobalConfig();
469
+ return config.defaultRegistry;
470
+ }
471
+ async function setDefaultRegistry(url) {
472
+ const config = await getGlobalConfig();
473
+ config.defaultRegistry = url;
474
+ await setGlobalConfig(config);
475
+ log.debug(`Set default registry to ${url}`);
476
+ }
477
+ async function clearDefaultRegistry() {
478
+ const config = await getGlobalConfig();
479
+ delete config.defaultRegistry;
480
+ await setGlobalConfig(config);
481
+ log.debug("Cleared default registry");
482
+ }
483
+ function createGit(baseDir) {
484
+ const options = {
485
+ baseDir: baseDir ? toUnix(baseDir) : void 0,
486
+ binary: "git",
487
+ maxConcurrentProcesses: 6,
488
+ trimmed: true,
489
+ timeout: {
490
+ block: 6e4
491
+ // 60 seconds for any single operation
492
+ }
493
+ };
494
+ return simpleGit(options);
495
+ }
496
+ async function checkAccess(repoUrl) {
497
+ log.debug(`Checking access to ${repoUrl}`);
498
+ try {
499
+ const git = createGit();
500
+ await git.listRemote([repoUrl, "HEAD"]);
501
+ log.debug(`Access confirmed for ${repoUrl}`);
502
+ return true;
503
+ } catch (error) {
504
+ const message = error instanceof Error ? error.message : "Unknown error";
505
+ log.debug(`Access denied or repo not found: ${repoUrl} - ${message}`);
506
+ return false;
507
+ }
508
+ }
509
+ async function getDefaultBranch(repoUrl) {
510
+ log.debug(`Detecting default branch for ${repoUrl}`);
511
+ try {
512
+ const git = createGit();
513
+ const result = await git.listRemote(["--symref", repoUrl, "HEAD"]);
514
+ const match = result.match(/ref:\s+refs\/heads\/([^\t\s]+)/);
515
+ if (match && match[1]) {
516
+ log.debug(`Default branch detected: ${match[1]}`);
517
+ return match[1];
518
+ }
519
+ const branchesResult = await git.listRemote(["--heads", repoUrl]);
520
+ const commonBranches = ["main", "master", "develop", "trunk"];
521
+ for (const branch of commonBranches) {
522
+ if (branchesResult.includes(`refs/heads/${branch}`)) {
523
+ log.debug(`Default branch fallback: ${branch}`);
524
+ return branch;
525
+ }
526
+ }
527
+ log.debug('Could not detect default branch, using "main"');
528
+ return "main";
529
+ } catch (error) {
530
+ const message = error instanceof Error ? error.message : "Unknown error";
531
+ log.debug(`Failed to detect default branch: ${message}, using "main"`);
532
+ return "main";
533
+ }
534
+ }
535
+ async function sparseClone(options) {
536
+ const { repoUrl, targetDir, sparsePath } = options;
537
+ const normalizedTarget = toUnix(targetDir);
538
+ log.debug(`Sparse cloning ${repoUrl} to ${normalizedTarget}`);
539
+ const branch = options.branch || await getDefaultBranch(repoUrl);
540
+ if (await exists(normalizedTarget)) {
541
+ if (await isGitRepo(normalizedTarget)) {
542
+ if (await isDirty(normalizedTarget)) {
543
+ throw new Error(
544
+ `Target directory has uncommitted changes: ${normalizedTarget}. Please commit or discard changes before re-cloning.`
545
+ );
546
+ }
547
+ }
548
+ log.debug(`Removing existing directory: ${normalizedTarget}`);
549
+ await fs2.remove(normalizedTarget);
550
+ }
551
+ await ensureDir(dirname(normalizedTarget));
552
+ const cloneArgs = [
553
+ "--filter=blob:none",
554
+ // Partial clone - no blobs initially
555
+ "--sparse",
556
+ // Enable sparse checkout
557
+ "--depth",
558
+ "1",
559
+ // Shallow clone - no history
560
+ "--branch",
561
+ branch
562
+ // Always specify branch (auto-detected if not provided)
563
+ ];
564
+ cloneArgs.push(repoUrl, normalizedTarget);
565
+ const git = createGit();
566
+ try {
567
+ await git.clone(repoUrl, normalizedTarget, cloneArgs.slice(0, -2));
568
+ } catch (error) {
569
+ const message = error instanceof Error ? error.message : "Unknown error";
570
+ throw new Error(`Failed to clone ${repoUrl}: ${message}`);
571
+ }
572
+ if (sparsePath) {
573
+ const repoGit = createGit(normalizedTarget);
574
+ const normalizedSparsePath = toUnix(sparsePath).replace(/^\/+/, "");
575
+ try {
576
+ await repoGit.raw(["sparse-checkout", "set", "--no-cone", normalizedSparsePath]);
577
+ log.debug(`Set sparse-checkout path: ${normalizedSparsePath}`);
578
+ } catch (error) {
579
+ const message = error instanceof Error ? error.message : "Unknown error";
580
+ throw new Error(`Failed to set sparse-checkout path '${sparsePath}': ${message}`);
581
+ }
582
+ const sparseFullPath = join(normalizedTarget, normalizedSparsePath);
583
+ if (!await exists(sparseFullPath)) {
584
+ log.warn(`Sparse path '${sparsePath}' does not exist in the repository`);
585
+ } else {
586
+ log.debug(`Flattening sparse path: ${sparseFullPath} -> ${normalizedTarget}`);
587
+ const items = await fs2.readdir(sparseFullPath);
588
+ for (const item of items) {
589
+ const srcPath = join(sparseFullPath, item);
590
+ const destPath = join(normalizedTarget, item);
591
+ await fs2.move(srcPath, destPath, { overwrite: true });
592
+ }
593
+ const topLevelDir = normalizedSparsePath.split("/")[0];
594
+ if (topLevelDir) {
595
+ const topLevelPath = join(normalizedTarget, topLevelDir);
596
+ await fs2.remove(topLevelPath);
597
+ log.debug(`Removed empty sparse directory: ${topLevelPath}`);
598
+ }
599
+ }
600
+ }
601
+ const commitHash = await getHeadCommit(normalizedTarget);
602
+ log.debug(`Clone complete. HEAD: ${commitHash}`);
603
+ return { commitHash };
604
+ }
605
+ async function isGitRepo(dirPath) {
606
+ const gitDir = join(dirPath, ".git");
607
+ return exists(gitDir);
608
+ }
609
+ async function isDirty(repoPath) {
610
+ const normalizedPath = toUnix(repoPath);
611
+ log.debug(`Checking dirty state for ${normalizedPath}`);
612
+ if (!await isGitRepo(normalizedPath)) {
613
+ throw new Error(`Not a git repository: ${normalizedPath}`);
614
+ }
615
+ try {
616
+ const git = createGit(normalizedPath);
617
+ const status = await git.status();
618
+ const isDirtyState = !status.isClean();
619
+ log.debug(`Repository ${normalizedPath} is ${isDirtyState ? "dirty" : "clean"}`);
620
+ return isDirtyState;
621
+ } catch (error) {
622
+ const message = error instanceof Error ? error.message : "Unknown error";
623
+ throw new Error(`Failed to check repository status: ${message}`);
624
+ }
625
+ }
626
+ async function getHeadCommit(repoPath) {
627
+ const normalizedPath = toUnix(repoPath);
628
+ if (!await isGitRepo(normalizedPath)) {
629
+ throw new Error(`Not a git repository: ${normalizedPath}`);
630
+ }
631
+ try {
632
+ const git = createGit(normalizedPath);
633
+ const hash = await git.revparse(["HEAD"]);
634
+ return hash.trim();
635
+ } catch (error) {
636
+ const message = error instanceof Error ? error.message : "Unknown error";
637
+ throw new Error(`Failed to get HEAD commit: ${message}`);
638
+ }
639
+ }
640
+ async function pull(repoPath) {
641
+ const normalizedPath = toUnix(repoPath);
642
+ log.debug(`Pulling updates for ${normalizedPath}`);
643
+ if (!await isGitRepo(normalizedPath)) {
644
+ throw new Error(`Not a git repository: ${normalizedPath}`);
645
+ }
646
+ if (await isDirty(normalizedPath)) {
647
+ throw new Error(
648
+ `Cannot pull: repository has uncommitted changes at ${normalizedPath}. Please commit or discard changes first.`
649
+ );
650
+ }
651
+ try {
652
+ const git = createGit(normalizedPath);
653
+ await git.pull();
654
+ const commitHash = await getHeadCommit(normalizedPath);
655
+ log.debug(`Pull complete. HEAD: ${commitHash}`);
656
+ return { commitHash };
657
+ } catch (error) {
658
+ const message = error instanceof Error ? error.message : "Unknown error";
659
+ throw new Error(`Failed to pull updates: ${message}`);
660
+ }
661
+ }
662
+ async function getRemoteCommitHash(repoUrl, branch) {
663
+ log.debug(`Getting remote commit hash for ${repoUrl}`);
664
+ try {
665
+ const git = createGit();
666
+ const ref = branch ? `refs/heads/${branch}` : "HEAD";
667
+ const result = await git.listRemote([repoUrl, ref]);
668
+ const match = result.match(/^([a-f0-9]+)/);
669
+ if (!match || !match[1]) {
670
+ throw new Error("Could not parse commit hash from remote");
671
+ }
672
+ const commitHash = match[1];
673
+ log.debug(`Remote commit hash: ${commitHash}`);
674
+ return commitHash;
675
+ } catch (error) {
676
+ const message = error instanceof Error ? error.message : "Unknown error";
677
+ throw new Error(`Failed to get remote commit hash: ${message}`);
678
+ }
679
+ }
680
+ async function cloneToTemp(repoUrl, branch) {
681
+ const tempBase = join(os.tmpdir(), "nocaap-push");
682
+ await ensureDir(tempBase);
683
+ const tempDir = await fs2.mkdtemp(join(tempBase, "repo-"));
684
+ log.debug(`Cloning ${repoUrl} to temp directory: ${tempDir}`);
685
+ try {
686
+ const git = createGit();
687
+ const cloneArgs = ["--depth", "1"];
688
+ if (branch) {
689
+ cloneArgs.push("--branch", branch);
690
+ }
691
+ await git.clone(repoUrl, tempDir, cloneArgs);
692
+ log.debug(`Temp clone complete: ${tempDir}`);
693
+ return {
694
+ tempDir,
695
+ cleanup: async () => {
696
+ log.debug(`Cleaning up temp directory: ${tempDir}`);
697
+ await fs2.remove(tempDir);
698
+ }
699
+ };
700
+ } catch (error) {
701
+ await fs2.remove(tempDir);
702
+ const message = error instanceof Error ? error.message : "Unknown error";
703
+ throw new Error(`Failed to clone to temp directory: ${message}`);
704
+ }
705
+ }
706
+ async function createBranch(repoPath, branchName) {
707
+ const normalizedPath = toUnix(repoPath);
708
+ if (!await isGitRepo(normalizedPath)) {
709
+ throw new Error(`Not a git repository: ${normalizedPath}`);
710
+ }
711
+ try {
712
+ const git = createGit(normalizedPath);
713
+ await git.checkoutLocalBranch(branchName);
714
+ log.debug(`Created and checked out branch: ${branchName}`);
715
+ } catch (error) {
716
+ const message = error instanceof Error ? error.message : "Unknown error";
717
+ throw new Error(`Failed to create branch: ${message}`);
718
+ }
719
+ }
720
+ async function commitAll(repoPath, message) {
721
+ const normalizedPath = toUnix(repoPath);
722
+ if (!await isGitRepo(normalizedPath)) {
723
+ throw new Error(`Not a git repository: ${normalizedPath}`);
724
+ }
725
+ try {
726
+ const git = createGit(normalizedPath);
727
+ await git.add("-A");
728
+ const result = await git.commit(message);
729
+ log.debug(`Committed changes: ${result.commit}`);
730
+ return result.commit;
731
+ } catch (error) {
732
+ const message2 = error instanceof Error ? error.message : "Unknown error";
733
+ throw new Error(`Failed to commit: ${message2}`);
734
+ }
735
+ }
736
+ async function pushBranch(repoPath, branchName) {
737
+ const normalizedPath = toUnix(repoPath);
738
+ if (!await isGitRepo(normalizedPath)) {
739
+ throw new Error(`Not a git repository: ${normalizedPath}`);
740
+ }
741
+ try {
742
+ const git = createGit(normalizedPath);
743
+ await git.push("origin", branchName, ["--set-upstream"]);
744
+ log.debug(`Pushed branch ${branchName} to origin`);
745
+ } catch (error) {
746
+ const message = error instanceof Error ? error.message : "Unknown error";
747
+ if (message.includes("Permission denied") || message.includes("403") || message.includes("authentication failed")) {
748
+ throw new Error(
749
+ `Permission denied. You don't have write access to this repository.
750
+ Consider forking the repository and pushing to your fork.`
751
+ );
752
+ }
753
+ throw new Error(`Failed to push: ${message}`);
754
+ }
755
+ }
756
+
757
+ // src/core/registry.ts
758
+ var DEFAULT_MAX_DEPTH = 3;
759
+ var DEFAULT_TIMEOUT_MS = 1e4;
760
+ function normalizeRegistryUrl(url) {
761
+ const original = url.trim();
762
+ let filePath = "nocaap-registry.json";
763
+ let gitUrl = "";
764
+ let httpUrl = null;
765
+ let provider = "unknown";
766
+ let branch = null;
767
+ if (original.startsWith("git@") || original.startsWith("ssh://")) {
768
+ const hashIndex = original.indexOf("#");
769
+ if (hashIndex !== -1) {
770
+ gitUrl = original.substring(0, hashIndex);
771
+ filePath = original.substring(hashIndex + 1);
772
+ } else {
773
+ gitUrl = original.endsWith(".git") ? original : `${original}.git`;
774
+ }
775
+ if (original.includes("github.com")) provider = "github";
776
+ else if (original.includes("gitlab.com")) provider = "gitlab";
777
+ else if (original.includes("bitbucket.org")) provider = "bitbucket";
778
+ if (provider === "github") {
779
+ const match = gitUrl.match(/git@github\.com:(.+?)(?:\.git)?$/);
780
+ if (match) {
781
+ httpUrl = `https://raw.githubusercontent.com/${match[1]}/main/${filePath}`;
782
+ }
783
+ }
784
+ return { original, gitUrl, filePath, httpUrl, provider, branch };
785
+ }
786
+ const rawGitHubMatch = original.match(
787
+ /^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)$/
788
+ );
789
+ if (rawGitHubMatch) {
790
+ const [, org, repo, branchName, path2] = rawGitHubMatch;
791
+ gitUrl = `git@github.com:${org}/${repo}.git`;
792
+ filePath = path2;
793
+ httpUrl = original;
794
+ provider = "github";
795
+ branch = branchName ?? null;
796
+ return { original, gitUrl, filePath, httpUrl, provider, branch };
797
+ }
798
+ const blobMatch = original.match(
799
+ /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/
800
+ );
801
+ if (blobMatch) {
802
+ const [, org, repo, branchName, path2] = blobMatch;
803
+ gitUrl = `git@github.com:${org}/${repo}.git`;
804
+ filePath = path2;
805
+ httpUrl = `https://raw.githubusercontent.com/${org}/${repo}/${branchName}/${path2}`;
806
+ provider = "github";
807
+ branch = branchName ?? null;
808
+ return { original, gitUrl, filePath, httpUrl, provider, branch };
809
+ }
810
+ const repoMatch = original.match(
811
+ /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(\/)?$/
812
+ );
813
+ if (repoMatch && repoMatch[1] && repoMatch[2]) {
814
+ const org = repoMatch[1];
815
+ const repo = repoMatch[2];
816
+ const cleanRepo = repo.replace(/\.git$/, "");
817
+ gitUrl = `git@github.com:${org}/${cleanRepo}.git`;
818
+ filePath = "nocaap-registry.json";
819
+ httpUrl = `https://raw.githubusercontent.com/${org}/${cleanRepo}/main/${filePath}`;
820
+ provider = "github";
821
+ return { original, gitUrl, filePath, httpUrl, provider, branch };
822
+ }
823
+ const gitlabMatch = original.match(
824
+ /^https:\/\/gitlab\.com\/([^/]+)\/([^/]+)/
825
+ );
826
+ if (gitlabMatch && gitlabMatch[1] && gitlabMatch[2]) {
827
+ const org = gitlabMatch[1];
828
+ const repo = gitlabMatch[2];
829
+ const cleanRepo = repo.replace(/\.git$/, "");
830
+ gitUrl = `git@gitlab.com:${org}/${cleanRepo}.git`;
831
+ provider = "gitlab";
832
+ const gitlabFileMatch = original.match(
833
+ /^https:\/\/gitlab\.com\/([^/]+)\/([^/]+)\/-\/blob\/([^/]+)\/(.+)$/
834
+ );
835
+ if (gitlabFileMatch && gitlabFileMatch[3] && gitlabFileMatch[4]) {
836
+ filePath = gitlabFileMatch[4];
837
+ branch = gitlabFileMatch[3];
838
+ httpUrl = `https://gitlab.com/${org}/${cleanRepo}/-/raw/${branch}/${filePath}`;
839
+ } else {
840
+ httpUrl = `https://gitlab.com/${org}/${cleanRepo}/-/raw/main/${filePath}`;
841
+ }
842
+ return { original, gitUrl, filePath, httpUrl, provider, branch };
843
+ }
844
+ const bitbucketMatch = original.match(
845
+ /^https:\/\/bitbucket\.org\/([^/]+)\/([^/]+)/
846
+ );
847
+ if (bitbucketMatch && bitbucketMatch[1] && bitbucketMatch[2]) {
848
+ const org = bitbucketMatch[1];
849
+ const repo = bitbucketMatch[2];
850
+ const cleanRepo = repo.replace(/\.git$/, "");
851
+ gitUrl = `git@bitbucket.org:${org}/${cleanRepo}.git`;
852
+ provider = "bitbucket";
853
+ httpUrl = `https://bitbucket.org/${org}/${cleanRepo}/raw/main/${filePath}`;
854
+ return { original, gitUrl, filePath, httpUrl, provider, branch };
855
+ }
856
+ if (original.startsWith("http://") || original.startsWith("https://")) {
857
+ return {
858
+ original,
859
+ gitUrl: "",
860
+ filePath: "",
861
+ httpUrl: original,
862
+ provider: "unknown",
863
+ branch: null
864
+ };
865
+ }
866
+ throw new Error(
867
+ `Unrecognized registry URL format: ${original}
868
+
869
+ Supported formats:
870
+ https://github.com/org/repo
871
+ https://github.com/org/repo/blob/main/nocaap-registry.json
872
+ https://raw.githubusercontent.com/org/repo/main/file.json
873
+ git@github.com:org/repo.git
874
+ git@github.com:org/repo.git#path/to/registry.json`
875
+ );
876
+ }
877
+ async function fetchRegistryViaGit(repoUrl, filePath, branchHint) {
878
+ log.debug(`Fetching registry via Git: ${repoUrl} -> ${filePath}`);
879
+ let hasAccess;
880
+ try {
881
+ hasAccess = await checkAccess(repoUrl);
882
+ } catch (error) {
883
+ const message = error instanceof Error ? error.message : "Unknown error";
884
+ if (message.includes("ENOENT") || message.includes("not found")) {
885
+ throw new Error(
886
+ `Git is not installed or not in PATH.
887
+
888
+ To use private repositories, you need:
889
+ 1. Git installed (https://git-scm.com)
890
+ 2. SSH keys configured for your Git host`
891
+ );
892
+ }
893
+ throw new Error(`Failed to check repository access: ${message}`);
894
+ }
895
+ if (!hasAccess) {
896
+ throw new Error(
897
+ `Cannot access repository via SSH: ${repoUrl}
898
+
899
+ Please check:
900
+ \u2022 You have SSH keys configured (run: ssh -T git@github.com)
901
+ \u2022 You have read access to the repository
902
+ \u2022 The repository URL is correct`
903
+ );
904
+ }
905
+ const tempDir = join(os.tmpdir(), `nocaap-registry-${Date.now()}`);
906
+ try {
907
+ const fileDir = dirname(filePath);
908
+ const sparsePath = fileDir === "." || fileDir === "" ? void 0 : fileDir;
909
+ const branch = branchHint || await getDefaultBranch(repoUrl);
910
+ await sparseClone({
911
+ repoUrl,
912
+ targetDir: tempDir,
913
+ sparsePath,
914
+ branch
915
+ });
916
+ const registryPath = join(tempDir, filePath);
917
+ if (!await exists(registryPath)) {
918
+ throw new Error(
919
+ `Registry file not found: ${filePath}
920
+
921
+ Please check:
922
+ \u2022 The file path is correct
923
+ \u2022 The file exists in the repository
924
+ \u2022 Try: git@...#${filePath}`
925
+ );
926
+ }
927
+ const content = await fs2.readFile(registryPath, "utf-8");
928
+ let data;
929
+ try {
930
+ data = JSON.parse(content);
931
+ } catch {
932
+ throw new Error(`Invalid JSON in registry file: ${filePath}`);
933
+ }
934
+ const result = safeValidateRegistry(data);
935
+ if (!result.success) {
936
+ const errorMessages = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
937
+ throw new Error(`Invalid registry schema: ${errorMessages}`);
938
+ }
939
+ log.debug(`Successfully fetched registry via Git with ${result.data.contexts.length} contexts`);
940
+ return result.data;
941
+ } finally {
942
+ await fs2.remove(tempDir).catch(() => {
943
+ log.debug(`Failed to cleanup temp directory: ${tempDir}`);
944
+ });
945
+ }
946
+ }
947
+ var HttpFetchError = class extends Error {
948
+ constructor(message, status, shouldTrySSH) {
949
+ super(message);
950
+ this.status = status;
951
+ this.shouldTrySSH = shouldTrySSH;
952
+ this.name = "HttpFetchError";
953
+ }
954
+ };
955
+ async function fetchRegistryViaHttp(url) {
956
+ log.debug(`Fetching registry via HTTP: ${url}`);
957
+ let response;
958
+ try {
959
+ const controller = new AbortController();
960
+ const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
961
+ response = await fetch(url, { signal: controller.signal });
962
+ clearTimeout(timeoutId);
963
+ } catch (error) {
964
+ if (error instanceof Error && error.name === "AbortError") {
965
+ throw new HttpFetchError(
966
+ `Registry fetch timed out after ${DEFAULT_TIMEOUT_MS / 1e3}s`,
967
+ null,
968
+ false
969
+ );
970
+ }
971
+ const message = error instanceof Error ? error.message : "Unknown error";
972
+ throw new HttpFetchError(`Network error: ${message}`, null, false);
973
+ }
974
+ if (!response.ok) {
975
+ const isGitHub = url.includes("githubusercontent.com") || url.includes("github.com");
976
+ const isGitLab = url.includes("gitlab.com");
977
+ const shouldTrySSH = (response.status === 404 || response.status === 403) && (isGitHub || isGitLab);
978
+ throw new HttpFetchError(
979
+ `HTTP ${response.status} ${response.statusText}`,
980
+ response.status,
981
+ shouldTrySSH
982
+ );
983
+ }
984
+ let data;
985
+ try {
986
+ data = await response.json();
987
+ } catch {
988
+ throw new HttpFetchError("Invalid JSON response", null, false);
989
+ }
990
+ const result = safeValidateRegistry(data);
991
+ if (!result.success) {
992
+ const errorMessages = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
993
+ throw new Error(`Invalid registry schema: ${errorMessages}`);
994
+ }
995
+ log.debug(`Successfully fetched registry via HTTP with ${result.data.contexts.length} contexts`);
996
+ return result.data;
997
+ }
998
+ async function fetchRegistry(registryUrl) {
999
+ const normalized = normalizeRegistryUrl(registryUrl);
1000
+ log.debug(`Normalized registry URL: ${JSON.stringify(normalized, null, 2)}`);
1001
+ if (!normalized.httpUrl && normalized.gitUrl) {
1002
+ log.debug("No HTTP URL available, using Git directly");
1003
+ return fetchRegistryViaGit(normalized.gitUrl, normalized.filePath, normalized.branch);
1004
+ }
1005
+ if (normalized.httpUrl) {
1006
+ try {
1007
+ log.debug(`Trying HTTP fetch: ${normalized.httpUrl}`);
1008
+ const registry = await fetchRegistryViaHttp(normalized.httpUrl);
1009
+ return registry;
1010
+ } catch (error) {
1011
+ if (error instanceof HttpFetchError && error.shouldTrySSH && normalized.gitUrl) {
1012
+ log.debug(`HTTP failed (${error.status}), trying SSH fallback`);
1013
+ try {
1014
+ return await fetchRegistryViaGit(
1015
+ normalized.gitUrl,
1016
+ normalized.filePath,
1017
+ normalized.branch
1018
+ );
1019
+ } catch (sshError) {
1020
+ const sshMessage = sshError instanceof Error ? sshError.message : "Unknown error";
1021
+ throw new Error(
1022
+ `Could not fetch registry from: ${registryUrl}
1023
+
1024
+ HTTP attempt: ${error.message}
1025
+ SSH attempt: ${sshMessage}
1026
+
1027
+ Possible solutions:
1028
+ \u2022 Check the URL is correct
1029
+ \u2022 For private repos: ensure SSH keys are configured
1030
+ \u2022 Run: ssh -T git@github.com (to test SSH access)`
1031
+ );
1032
+ }
1033
+ }
1034
+ if (error instanceof HttpFetchError) {
1035
+ const hint = error.shouldTrySSH ? `
1036
+
1037
+ This might be a private repository. Try using SSH:
1038
+ nocaap config registry ${normalized.gitUrl || "git@github.com:org/repo.git"}` : "";
1039
+ throw new Error(
1040
+ `Failed to fetch registry: ${error.message}${hint}`
1041
+ );
1042
+ }
1043
+ throw error;
1044
+ }
1045
+ }
1046
+ throw new Error(`Could not determine how to fetch registry from: ${registryUrl}`);
1047
+ }
1048
+ async function fetchRegistryWithImports(url, options) {
1049
+ const visited = options?.visited ?? /* @__PURE__ */ new Set();
1050
+ const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
1051
+ const currentDepth = options?.currentDepth ?? 0;
1052
+ const normalizedUrl = url.replace(/\/$/, "").replace(/#.*$/, "");
1053
+ if (visited.has(normalizedUrl)) {
1054
+ log.warn(`Circular import detected, skipping: ${url}`);
1055
+ return { contexts: [] };
1056
+ }
1057
+ if (currentDepth >= maxDepth) {
1058
+ log.warn(`Max import depth (${maxDepth}) exceeded, skipping: ${url}`);
1059
+ return { contexts: [] };
1060
+ }
1061
+ visited.add(normalizedUrl);
1062
+ const registry = await fetchRegistry(url);
1063
+ if (!registry.imports || registry.imports.length === 0) {
1064
+ return registry;
1065
+ }
1066
+ log.debug(`Registry has ${registry.imports.length} imports, fetching...`);
1067
+ const importPromises = registry.imports.map(async (importUrl) => {
1068
+ try {
1069
+ return await fetchRegistryWithImports(importUrl, {
1070
+ visited,
1071
+ maxDepth,
1072
+ currentDepth: currentDepth + 1
1073
+ });
1074
+ } catch (error) {
1075
+ const message = error instanceof Error ? error.message : "Unknown error";
1076
+ log.warn(`Failed to fetch imported registry ${importUrl}: ${message}`);
1077
+ return { contexts: [] };
1078
+ }
1079
+ });
1080
+ const importedRegistries = await Promise.all(importPromises);
1081
+ return mergeRegistries([registry, ...importedRegistries]);
1082
+ }
1083
+ function getContextKey(context) {
1084
+ const path2 = context.path ?? "";
1085
+ return `${context.repo}::${path2}`;
1086
+ }
1087
+ function mergeRegistries(registries) {
1088
+ const contextMap = /* @__PURE__ */ new Map();
1089
+ for (const registry of registries) {
1090
+ for (const context of registry.contexts) {
1091
+ const key = getContextKey(context);
1092
+ contextMap.set(key, context);
1093
+ }
1094
+ }
1095
+ const mergedContexts = Array.from(contextMap.values());
1096
+ log.debug(`Merged ${registries.length} registries into ${mergedContexts.length} unique contexts`);
1097
+ return {
1098
+ name: registries[0]?.name,
1099
+ // Preserve root registry name
1100
+ contexts: mergedContexts
1101
+ // Don't include imports in merged result (they've been resolved)
1102
+ };
1103
+ }
1104
+ var CHARS_PER_TOKEN = 4;
1105
+ var TOKEN_BUDGET_WARNING = 8e3;
1106
+ var MAX_PREVIEW_LINES = 5;
1107
+ var DOC_EXTENSIONS = [".md", ".mdx"];
1108
+ async function parseDocFile(filePath, basePath) {
1109
+ const normalizedPath = toUnix(filePath);
1110
+ const relativePath = relative(basePath, normalizedPath);
1111
+ log.debug(`Parsing doc file: ${relativePath}`);
1112
+ const content = await fs2.readFile(normalizedPath, "utf-8");
1113
+ const { data: frontmatter, content: body } = matter(content);
1114
+ const title = extractTitle(frontmatter, body, normalizedPath);
1115
+ const summary = frontmatter.summary ?? frontmatter.description;
1116
+ const type = frontmatter.type;
1117
+ const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags.filter((t) => typeof t === "string") : void 0;
1118
+ const preview = summary || extractPreview(body);
1119
+ return {
1120
+ title,
1121
+ summary,
1122
+ type,
1123
+ tags,
1124
+ relativePath,
1125
+ preview
1126
+ };
1127
+ }
1128
+ function extractTitle(frontmatter, body, filePath) {
1129
+ if (typeof frontmatter.title === "string" && frontmatter.title.trim()) {
1130
+ return frontmatter.title.trim();
1131
+ }
1132
+ const h1Match = body.match(/^#\s+(.+)$/m);
1133
+ if (h1Match?.[1]) {
1134
+ return h1Match[1].trim();
1135
+ }
1136
+ const filename = basename(filePath, extname(filePath));
1137
+ return filename.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
1138
+ }
1139
+ function extractPreview(body) {
1140
+ const lines = body.split("\n");
1141
+ const previewLines = [];
1142
+ for (const line of lines) {
1143
+ const trimmed = line.trim();
1144
+ if (!trimmed || trimmed.startsWith("#")) {
1145
+ continue;
1146
+ }
1147
+ if (trimmed === "---") {
1148
+ continue;
1149
+ }
1150
+ previewLines.push(trimmed);
1151
+ if (previewLines.length >= MAX_PREVIEW_LINES) {
1152
+ break;
1153
+ }
1154
+ }
1155
+ let preview = previewLines.join(" ");
1156
+ if (preview.length > 300) {
1157
+ preview = preview.slice(0, 300);
1158
+ const lastSpace = preview.lastIndexOf(" ");
1159
+ if (lastSpace > 200) {
1160
+ preview = preview.slice(0, lastSpace);
1161
+ }
1162
+ preview += "...";
1163
+ }
1164
+ return preview;
1165
+ }
1166
+ async function findDocFiles(dirPath) {
1167
+ const results = [];
1168
+ if (!await exists(dirPath)) {
1169
+ return results;
1170
+ }
1171
+ const entries = await fs2.readdir(dirPath, { withFileTypes: true });
1172
+ for (const entry of entries) {
1173
+ const fullPath = join(dirPath, entry.name);
1174
+ if (entry.isDirectory()) {
1175
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
1176
+ continue;
1177
+ }
1178
+ const subFiles = await findDocFiles(fullPath);
1179
+ results.push(...subFiles);
1180
+ } else if (entry.isFile()) {
1181
+ const ext = extname(entry.name).toLowerCase();
1182
+ if (DOC_EXTENSIONS.includes(ext)) {
1183
+ results.push(fullPath);
1184
+ }
1185
+ }
1186
+ }
1187
+ return results;
1188
+ }
1189
+ async function generateIndex(projectRoot) {
1190
+ const contextDir = getContextDir(projectRoot);
1191
+ getPackagesDir(projectRoot);
1192
+ const warnings = [];
1193
+ log.debug(`Generating index for ${projectRoot}`);
1194
+ const config = await readConfig(projectRoot);
1195
+ if (!config || config.packages.length === 0) {
1196
+ log.debug("No packages configured, generating empty index");
1197
+ return {
1198
+ content: generateEmptyIndex(),
1199
+ fileCount: 0,
1200
+ tokenEstimate: 0,
1201
+ warnings: ["No packages configured"]
1202
+ };
1203
+ }
1204
+ const packageIndexes = [];
1205
+ let totalFiles = 0;
1206
+ for (const pkg of config.packages) {
1207
+ const packagePath = getPackagePath(projectRoot, pkg.alias);
1208
+ if (!await exists(packagePath)) {
1209
+ log.debug(`Package directory not found: ${pkg.alias}`);
1210
+ warnings.push(`Package '${pkg.alias}' directory not found`);
1211
+ continue;
1212
+ }
1213
+ const docFiles = await findDocFiles(packagePath);
1214
+ if (docFiles.length === 0) {
1215
+ log.debug(`No documentation files found in package: ${pkg.alias}`);
1216
+ warnings.push(`No .md/.mdx files found in '${pkg.alias}'`);
1217
+ continue;
1218
+ }
1219
+ const fileMetas = [];
1220
+ for (const filePath of docFiles) {
1221
+ try {
1222
+ const meta = await parseDocFile(filePath, contextDir);
1223
+ fileMetas.push(meta);
1224
+ } catch (error) {
1225
+ const message = error instanceof Error ? error.message : "Unknown error";
1226
+ log.debug(`Failed to parse ${filePath}: ${message}`);
1227
+ warnings.push(`Failed to parse: ${relative(contextDir, filePath)}`);
1228
+ }
1229
+ }
1230
+ fileMetas.sort((a, b) => a.title.localeCompare(b.title));
1231
+ packageIndexes.push({
1232
+ alias: pkg.alias,
1233
+ files: fileMetas
1234
+ });
1235
+ totalFiles += fileMetas.length;
1236
+ }
1237
+ packageIndexes.sort((a, b) => a.alias.localeCompare(b.alias));
1238
+ const content = generateIndexMarkdown(packageIndexes);
1239
+ const tokenEstimate = Math.ceil(content.length / CHARS_PER_TOKEN);
1240
+ if (tokenEstimate > TOKEN_BUDGET_WARNING) {
1241
+ warnings.push(
1242
+ `INDEX.md exceeds recommended token budget: ~${tokenEstimate.toLocaleString()} tokens (recommended: <${TOKEN_BUDGET_WARNING.toLocaleString()})`
1243
+ );
1244
+ }
1245
+ log.debug(
1246
+ `Index generated: ${totalFiles} files, ~${tokenEstimate.toLocaleString()} tokens`
1247
+ );
1248
+ return {
1249
+ content,
1250
+ fileCount: totalFiles,
1251
+ tokenEstimate,
1252
+ warnings
1253
+ };
1254
+ }
1255
+ function generateEmptyIndex() {
1256
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1257
+ return `# Context Index
1258
+
1259
+ > Auto-generated by nocaap. Last updated: ${timestamp}
1260
+
1261
+ No packages configured. Run \`nocaap setup\` or \`nocaap add <repo>\` to add context packages.
1262
+ `;
1263
+ }
1264
+ function generateIndexMarkdown(packages) {
1265
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1266
+ const lines = [
1267
+ "# Context Index",
1268
+ "",
1269
+ `> Auto-generated by nocaap. Last updated: ${timestamp}`,
1270
+ ""
1271
+ ];
1272
+ if (packages.length > 1) {
1273
+ lines.push("## Table of Contents");
1274
+ lines.push("");
1275
+ for (const pkg of packages) {
1276
+ const anchorId = pkg.alias.toLowerCase().replace(/[^a-z0-9]+/g, "-");
1277
+ lines.push(`- [${pkg.alias}](#${anchorId}) (${pkg.files.length} files)`);
1278
+ }
1279
+ lines.push("");
1280
+ lines.push("---");
1281
+ lines.push("");
1282
+ }
1283
+ for (const pkg of packages) {
1284
+ lines.push(`## ${pkg.alias} (${pkg.files.length} files)`);
1285
+ lines.push("");
1286
+ for (const file of pkg.files) {
1287
+ lines.push(`### ${file.title}`);
1288
+ lines.push("");
1289
+ lines.push(`**Path:** \`${file.relativePath}\``);
1290
+ if (file.type) {
1291
+ lines.push(`**Type:** ${file.type}`);
1292
+ }
1293
+ if (file.tags && file.tags.length > 0) {
1294
+ lines.push(`**Tags:** ${file.tags.join(", ")}`);
1295
+ }
1296
+ lines.push("");
1297
+ lines.push(file.preview);
1298
+ lines.push("");
1299
+ lines.push("---");
1300
+ lines.push("");
1301
+ }
1302
+ }
1303
+ return lines.join("\n");
1304
+ }
1305
+ async function writeIndex(projectRoot) {
1306
+ const result = await generateIndex(projectRoot);
1307
+ const indexPath = getIndexPath(projectRoot);
1308
+ await fs2.writeFile(indexPath, result.content, "utf-8");
1309
+ log.debug(`Wrote INDEX.md to ${indexPath}`);
1310
+ return result;
1311
+ }
1312
+ async function generateIndexWithProgress(projectRoot) {
1313
+ log.info("Regenerating INDEX.md...");
1314
+ const result = await writeIndex(projectRoot);
1315
+ if (result.fileCount === 0) {
1316
+ log.warn("No documentation files found");
1317
+ } else {
1318
+ log.success(
1319
+ `Generated INDEX.md: ${result.fileCount} files, ~${result.tokenEstimate.toLocaleString()} tokens`
1320
+ );
1321
+ }
1322
+ for (const warning of result.warnings) {
1323
+ log.warn(warning);
1324
+ }
1325
+ return result;
1326
+ }
1327
+
1328
+ // src/commands/setup.ts
1329
+ var DEFAULT_REGISTRY_PROMPT = "Enter your organization's registry URL:";
1330
+ async function setupCommand(options) {
1331
+ const projectRoot = process.cwd();
1332
+ log.title("nocaap Setup Wizard");
1333
+ log.newline();
1334
+ if (await configExists(projectRoot)) {
1335
+ const config = await readConfig(projectRoot);
1336
+ if (config && config.packages.length > 0) {
1337
+ log.warn("nocaap is already configured in this project.");
1338
+ log.dim(` ${config.packages.length} package(s) installed`);
1339
+ log.newline();
1340
+ const shouldContinue = await confirm({
1341
+ message: "Do you want to add more packages?",
1342
+ default: true
1343
+ });
1344
+ if (!shouldContinue) {
1345
+ log.info("Setup cancelled.");
1346
+ return;
1347
+ }
1348
+ }
1349
+ }
1350
+ let registryUrl = options.registry;
1351
+ if (!registryUrl) {
1352
+ const defaultRegistry = await getDefaultRegistry();
1353
+ if (defaultRegistry) {
1354
+ log.info(`Using default registry: ${style.url(defaultRegistry)}`);
1355
+ log.dim("(from global config - run `nocaap config registry` to change)");
1356
+ log.newline();
1357
+ const useDefault = await confirm({
1358
+ message: "Use this registry?",
1359
+ default: true
1360
+ });
1361
+ if (useDefault) {
1362
+ registryUrl = defaultRegistry;
1363
+ }
1364
+ }
1365
+ }
1366
+ if (!registryUrl) {
1367
+ registryUrl = await input({
1368
+ message: DEFAULT_REGISTRY_PROMPT,
1369
+ validate: (value) => {
1370
+ if (!value.trim()) {
1371
+ return "Registry URL is required";
1372
+ }
1373
+ try {
1374
+ new URL(value);
1375
+ return true;
1376
+ } catch {
1377
+ return "Please enter a valid URL";
1378
+ }
1379
+ }
1380
+ });
1381
+ log.newline();
1382
+ const saveAsDefault = await confirm({
1383
+ message: "Save this as your default registry?",
1384
+ default: true
1385
+ });
1386
+ if (saveAsDefault) {
1387
+ await setDefaultRegistry(registryUrl);
1388
+ log.success("Saved to global config!");
1389
+ }
1390
+ }
1391
+ log.newline();
1392
+ const fetchSpinner = createSpinner("Fetching registry...").start();
1393
+ let registry;
1394
+ try {
1395
+ registry = await fetchRegistryWithImports(registryUrl);
1396
+ fetchSpinner.succeed(
1397
+ `Fetched registry: ${registry.contexts.length} context(s) available`
1398
+ );
1399
+ } catch (error) {
1400
+ fetchSpinner.fail("Failed to fetch registry");
1401
+ throw error;
1402
+ }
1403
+ if (registry.contexts.length === 0) {
1404
+ log.warn("No contexts found in registry.");
1405
+ return;
1406
+ }
1407
+ log.newline();
1408
+ const accessSpinner = createSpinner("Checking repository access...").start();
1409
+ const accessResults = [];
1410
+ for (const context of registry.contexts) {
1411
+ const hasAccess = await checkAccess(context.repo);
1412
+ accessResults.push({ context, hasAccess });
1413
+ }
1414
+ const accessibleContexts = accessResults.filter((r) => r.hasAccess);
1415
+ const inaccessibleContexts = accessResults.filter((r) => !r.hasAccess);
1416
+ accessSpinner.succeed(
1417
+ `Access check complete: ${accessibleContexts.length} accessible, ${inaccessibleContexts.length} restricted`
1418
+ );
1419
+ if (inaccessibleContexts.length > 0) {
1420
+ log.newline();
1421
+ log.dim("Restricted contexts (no access):");
1422
+ for (const { context } of inaccessibleContexts) {
1423
+ log.dim(` - ${context.name}`);
1424
+ }
1425
+ }
1426
+ if (accessibleContexts.length === 0) {
1427
+ log.newline();
1428
+ log.error("No accessible contexts found. Check your SSH keys and permissions.");
1429
+ return;
1430
+ }
1431
+ log.newline();
1432
+ const choices = accessibleContexts.map(({ context }) => ({
1433
+ name: formatContextChoice(context),
1434
+ value: context.name,
1435
+ checked: false
1436
+ }));
1437
+ const selectedNames = await checkbox({
1438
+ message: "Select contexts to install:",
1439
+ choices,
1440
+ pageSize: 15
1441
+ });
1442
+ if (selectedNames.length === 0) {
1443
+ log.warn("No contexts selected. Setup cancelled.");
1444
+ return;
1445
+ }
1446
+ log.newline();
1447
+ if (!await configExists(projectRoot)) {
1448
+ const initSpinner = createSpinner("Initializing .context/ directory...").start();
1449
+ await initContextDir(projectRoot);
1450
+ initSpinner.succeed("Initialized .context/ directory");
1451
+ }
1452
+ const existingConfig = await readConfig(projectRoot) ?? { packages: [] };
1453
+ existingConfig.registryUrl = registryUrl;
1454
+ await writeConfig(projectRoot, existingConfig);
1455
+ log.newline();
1456
+ log.info(`Installing ${selectedNames.length} context(s)...`);
1457
+ log.newline();
1458
+ let successCount = 0;
1459
+ let failCount = 0;
1460
+ for (const name of selectedNames) {
1461
+ const context = accessibleContexts.find((r) => r.context.name === name)?.context;
1462
+ if (!context) continue;
1463
+ const alias = generateAlias(context);
1464
+ const spinner = createSpinner(`Installing ${style.bold(context.name)}...`).start();
1465
+ try {
1466
+ const targetDir = getPackagePath(projectRoot, alias);
1467
+ const { commitHash } = await sparseClone({
1468
+ repoUrl: context.repo,
1469
+ targetDir,
1470
+ sparsePath: context.path
1471
+ });
1472
+ await upsertPackage(projectRoot, {
1473
+ alias,
1474
+ source: context.repo,
1475
+ path: context.path,
1476
+ version: "main"
1477
+ });
1478
+ await updateLockEntry(projectRoot, alias, {
1479
+ commitHash,
1480
+ sparsePath: context.path || "",
1481
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1482
+ });
1483
+ spinner.succeed(`Installed ${context.name} \u2192 ${alias}`);
1484
+ successCount++;
1485
+ } catch (error) {
1486
+ const message = error instanceof Error ? error.message : "Unknown error";
1487
+ spinner.fail(`Failed to install ${context.name}: ${message}`);
1488
+ failCount++;
1489
+ }
1490
+ }
1491
+ log.newline();
1492
+ if (successCount > 0) {
1493
+ await generateIndexWithProgress(projectRoot);
1494
+ }
1495
+ if (successCount > 0) {
1496
+ log.newline();
1497
+ log.hr();
1498
+ log.newline();
1499
+ log.info("IDE Integration (optional)");
1500
+ log.newline();
1501
+ const addCursor = await confirm({
1502
+ message: "Add nocaap reference to Cursor rules?",
1503
+ default: true
1504
+ });
1505
+ if (addCursor) {
1506
+ const updated = await updateCursorRules(projectRoot);
1507
+ if (updated) {
1508
+ log.success("Added nocaap reference to Cursor rules");
1509
+ } else {
1510
+ log.dim("Cursor rules already configured");
1511
+ }
1512
+ }
1513
+ const addClaude = await confirm({
1514
+ message: "Add nocaap reference to CLAUDE.md?",
1515
+ default: true
1516
+ });
1517
+ if (addClaude) {
1518
+ const updated = await updateClaudeMd(projectRoot);
1519
+ if (updated) {
1520
+ log.success("Added nocaap reference to CLAUDE.md");
1521
+ } else {
1522
+ log.dim("CLAUDE.md already configured");
1523
+ }
1524
+ }
1525
+ }
1526
+ log.newline();
1527
+ log.hr();
1528
+ log.newline();
1529
+ if (successCount > 0) {
1530
+ log.success(`Setup complete! ${successCount} context(s) installed.`);
1531
+ log.newline();
1532
+ log.info("Next steps:");
1533
+ log.dim(" 1. Review .context/INDEX.md for available documentation");
1534
+ log.dim(" 2. Run `nocaap update` to pull latest changes");
1535
+ }
1536
+ if (failCount > 0) {
1537
+ log.newline();
1538
+ log.warn(`${failCount} context(s) failed to install.`);
1539
+ }
1540
+ }
1541
+ function formatContextChoice(context) {
1542
+ let display = context.name;
1543
+ if (context.description) {
1544
+ display += ` - ${context.description}`;
1545
+ }
1546
+ if (context.tags && context.tags.length > 0) {
1547
+ display += ` [${context.tags.join(", ")}]`;
1548
+ }
1549
+ return display;
1550
+ }
1551
+ function generateAlias(context) {
1552
+ if (context.path) {
1553
+ const leafFolder = context.path.split("/").filter(Boolean).pop();
1554
+ if (leafFolder) {
1555
+ return leafFolder.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
1556
+ }
1557
+ }
1558
+ return context.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
1559
+ }
1560
+
1561
+ // src/commands/add.ts
1562
+ function extractAliasFromUrl(url) {
1563
+ const cleaned = url.replace(/\.git$/, "");
1564
+ const segments = cleaned.split(/[/:]/);
1565
+ const lastSegment = segments[segments.length - 1];
1566
+ return lastSegment?.replace(/[^a-zA-Z0-9-_]/g, "") || "context";
1567
+ }
1568
+ function deriveAlias(url, path2) {
1569
+ if (path2) {
1570
+ const leafFolder = path2.split("/").filter(Boolean).pop();
1571
+ if (leafFolder) {
1572
+ return leafFolder.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
1573
+ }
1574
+ }
1575
+ return extractAliasFromUrl(url);
1576
+ }
1577
+ function validateAlias(alias) {
1578
+ return /^[a-zA-Z0-9][a-zA-Z0-9-_]{0,49}$/.test(alias);
1579
+ }
1580
+ async function addCommand(repo, options) {
1581
+ const projectRoot = process.cwd();
1582
+ const alias = options.alias || deriveAlias(repo, options.path);
1583
+ log.title("Adding context package");
1584
+ log.info(`Repository: ${repo}`);
1585
+ log.info(`Alias: ${alias}`);
1586
+ if (options.path) {
1587
+ log.info(`Path: ${options.path}`);
1588
+ }
1589
+ if (options.branch && options.branch !== "main") {
1590
+ log.info(`Branch: ${options.branch}`);
1591
+ }
1592
+ log.newline();
1593
+ if (!validateAlias(alias)) {
1594
+ throw new Error(
1595
+ `Invalid alias '${alias}'. Must be alphanumeric (can include - or _), 1-50 characters.`
1596
+ );
1597
+ }
1598
+ const accessSpinner = createSpinner("Checking repository access...").start();
1599
+ const hasAccess = await checkAccess(repo);
1600
+ if (!hasAccess) {
1601
+ accessSpinner.fail("Repository access denied");
1602
+ throw new Error(
1603
+ `Cannot access repository: ${repo}
1604
+ Please check:
1605
+ - The URL is correct
1606
+ - You have SSH keys configured (for git@ URLs)
1607
+ - You have read access to the repository`
1608
+ );
1609
+ }
1610
+ accessSpinner.succeed("Repository access confirmed");
1611
+ if (!await configExists(projectRoot)) {
1612
+ const initSpinner = createSpinner("Initializing .context/ directory...").start();
1613
+ await initContextDir(projectRoot);
1614
+ initSpinner.succeed("Initialized .context/ directory");
1615
+ }
1616
+ const targetDir = getPackagePath(projectRoot, alias);
1617
+ const cloneSpinner = createSpinner("Cloning repository...").start();
1618
+ try {
1619
+ const { commitHash } = await sparseClone({
1620
+ repoUrl: repo,
1621
+ targetDir,
1622
+ sparsePath: options.path,
1623
+ branch: options.branch
1624
+ });
1625
+ cloneSpinner.succeed(`Cloned to .context/packages/${alias}`);
1626
+ const configSpinner = createSpinner("Updating configuration...").start();
1627
+ await upsertPackage(projectRoot, {
1628
+ alias,
1629
+ source: repo,
1630
+ path: options.path,
1631
+ version: options.branch || "main"
1632
+ });
1633
+ await updateLockEntry(projectRoot, alias, {
1634
+ commitHash,
1635
+ sparsePath: options.path || "",
1636
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1637
+ });
1638
+ configSpinner.succeed("Configuration updated");
1639
+ log.newline();
1640
+ await generateIndexWithProgress(projectRoot);
1641
+ log.newline();
1642
+ log.success(`Package '${alias}' added successfully!`);
1643
+ log.dim(` Location: .context/packages/${alias}`);
1644
+ log.dim(` Commit: ${commitHash.slice(0, 8)}`);
1645
+ } catch (error) {
1646
+ cloneSpinner.fail("Clone failed");
1647
+ throw error;
1648
+ }
1649
+ }
1650
+
1651
+ // src/commands/update.ts
1652
+ async function updateCommand(alias, options) {
1653
+ const projectRoot = process.cwd();
1654
+ log.title("Updating context packages");
1655
+ const config = await readConfig(projectRoot);
1656
+ if (!config || config.packages.length === 0) {
1657
+ throw new Error(
1658
+ "No packages configured. Run `nocaap setup` or `nocaap add <repo>` first."
1659
+ );
1660
+ }
1661
+ const packagesToUpdate = alias ? config.packages.filter((p) => p.alias === alias) : config.packages;
1662
+ if (alias && packagesToUpdate.length === 0) {
1663
+ throw new Error(`Package '${alias}' not found in configuration.`);
1664
+ }
1665
+ log.info(`Updating ${packagesToUpdate.length} package(s)...`);
1666
+ log.newline();
1667
+ const results = [];
1668
+ for (const pkg of packagesToUpdate) {
1669
+ const result = await updatePackage(projectRoot, pkg, options);
1670
+ results.push(result);
1671
+ }
1672
+ log.newline();
1673
+ log.hr();
1674
+ log.newline();
1675
+ const updated = results.filter((r) => r.status === "updated");
1676
+ const upToDate = results.filter((r) => r.status === "up-to-date");
1677
+ const skipped = results.filter((r) => r.status === "skipped");
1678
+ const errors = results.filter((r) => r.status === "error");
1679
+ if (updated.length > 0) {
1680
+ log.success(`${updated.length} package(s) updated`);
1681
+ for (const r of updated) {
1682
+ log.dim(` ${r.alias}: ${r.oldCommit?.slice(0, 8)} \u2192 ${r.newCommit?.slice(0, 8)}`);
1683
+ }
1684
+ }
1685
+ if (upToDate.length > 0) {
1686
+ log.info(`${upToDate.length} package(s) already up-to-date`);
1687
+ }
1688
+ if (skipped.length > 0) {
1689
+ log.warn(`${skipped.length} package(s) skipped`);
1690
+ for (const r of skipped) {
1691
+ log.dim(` ${r.alias}: ${r.error}`);
1692
+ }
1693
+ }
1694
+ if (errors.length > 0) {
1695
+ log.error(`${errors.length} package(s) failed`);
1696
+ for (const r of errors) {
1697
+ log.dim(` ${r.alias}: ${r.error}`);
1698
+ }
1699
+ }
1700
+ if (updated.length > 0) {
1701
+ log.newline();
1702
+ await generateIndexWithProgress(projectRoot);
1703
+ }
1704
+ if (errors.length > 0) {
1705
+ throw new Error(`${errors.length} package(s) failed to update`);
1706
+ }
1707
+ }
1708
+ async function updatePackage(projectRoot, pkg, options) {
1709
+ const packagePath = getPackagePath(projectRoot, pkg.alias);
1710
+ const spinner = createSpinner(`Updating ${style.bold(pkg.alias)}...`).start();
1711
+ try {
1712
+ if (!await exists(packagePath)) {
1713
+ spinner.warn(`${pkg.alias}: Package directory not found`);
1714
+ return {
1715
+ alias: pkg.alias,
1716
+ status: "skipped",
1717
+ error: "Directory not found"
1718
+ };
1719
+ }
1720
+ if (!await isGitRepo(packagePath)) {
1721
+ spinner.warn(`${pkg.alias}: Not a git repository`);
1722
+ return {
1723
+ alias: pkg.alias,
1724
+ status: "skipped",
1725
+ error: "Not a git repository"
1726
+ };
1727
+ }
1728
+ if (await isDirty(packagePath)) {
1729
+ spinner.warn(`${pkg.alias}: Has uncommitted changes`);
1730
+ return {
1731
+ alias: pkg.alias,
1732
+ status: "skipped",
1733
+ error: "Uncommitted changes (commit or discard first)"
1734
+ };
1735
+ }
1736
+ const lockEntry = await getLockEntry(projectRoot, pkg.alias);
1737
+ if (lockEntry && lockEntry.sparsePath !== (pkg.path || "")) {
1738
+ spinner.warn(`${pkg.alias}: Sparse path changed in config`);
1739
+ log.dim(` Config: ${pkg.path || "(root)"}`);
1740
+ log.dim(` Locked: ${lockEntry.sparsePath || "(root)"}`);
1741
+ return {
1742
+ alias: pkg.alias,
1743
+ status: "skipped",
1744
+ error: "Sparse path changed (run `nocaap remove` then `nocaap add` to re-clone)"
1745
+ };
1746
+ }
1747
+ const oldCommit = await getHeadCommit(packagePath);
1748
+ const { commitHash: newCommit } = await pull(packagePath);
1749
+ if (oldCommit === newCommit && !options.force) {
1750
+ spinner.info(`${pkg.alias}: Already up-to-date`);
1751
+ return {
1752
+ alias: pkg.alias,
1753
+ status: "up-to-date",
1754
+ oldCommit,
1755
+ newCommit
1756
+ };
1757
+ }
1758
+ await updateLockEntry(projectRoot, pkg.alias, {
1759
+ commitHash: newCommit,
1760
+ sparsePath: pkg.path || "",
1761
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1762
+ });
1763
+ spinner.succeed(`${pkg.alias}: Updated`);
1764
+ return {
1765
+ alias: pkg.alias,
1766
+ status: "updated",
1767
+ oldCommit,
1768
+ newCommit
1769
+ };
1770
+ } catch (error) {
1771
+ const message = error instanceof Error ? error.message : "Unknown error";
1772
+ spinner.fail(`${pkg.alias}: Failed`);
1773
+ return {
1774
+ alias: pkg.alias,
1775
+ status: "error",
1776
+ error: message
1777
+ };
1778
+ }
1779
+ }
1780
+
1781
+ // src/commands/list.ts
1782
+ async function listCommand() {
1783
+ const projectRoot = process.cwd();
1784
+ const config = await readConfig(projectRoot);
1785
+ const lockfile = await readLockfile(projectRoot);
1786
+ if (!config || config.packages.length === 0) {
1787
+ log.info("No packages installed.");
1788
+ log.dim("Run `nocaap setup` or `nocaap add <repo>` to get started.");
1789
+ return;
1790
+ }
1791
+ log.title("Installed Packages");
1792
+ if (config.registryUrl) {
1793
+ log.dim(`Registry: ${config.registryUrl}`);
1794
+ log.newline();
1795
+ }
1796
+ for (const pkg of config.packages) {
1797
+ const lock = lockfile[pkg.alias];
1798
+ const commit = lock?.commitHash?.slice(0, 8) ?? "unknown";
1799
+ const packagePath = getPackagePath(projectRoot, pkg.alias);
1800
+ let statusIndicator = style.success("\u25CF");
1801
+ let statusText = "";
1802
+ if (!await exists(packagePath)) {
1803
+ statusIndicator = style.error("\u25CB");
1804
+ statusText = " (missing)";
1805
+ } else if (await isGitRepo(packagePath)) {
1806
+ try {
1807
+ if (await isDirty(packagePath)) {
1808
+ statusIndicator = style.warn("\u25CF");
1809
+ statusText = " (modified)";
1810
+ }
1811
+ } catch {
1812
+ }
1813
+ }
1814
+ log.plain(`${statusIndicator} ${style.bold(pkg.alias)}${statusText}`);
1815
+ log.dim(` Source: ${pkg.source}`);
1816
+ if (pkg.path) {
1817
+ log.dim(` Path: ${pkg.path}`);
1818
+ }
1819
+ log.dim(` Branch: ${pkg.version || "main"}`);
1820
+ log.dim(` Commit: ${commit}`);
1821
+ if (lock?.updatedAt) {
1822
+ const updated = new Date(lock.updatedAt).toLocaleDateString();
1823
+ log.dim(` Updated: ${updated}`);
1824
+ }
1825
+ log.newline();
1826
+ }
1827
+ log.hr();
1828
+ log.dim(`${config.packages.length} package(s) installed`);
1829
+ }
1830
+ async function removeCommand(alias, options) {
1831
+ const projectRoot = process.cwd();
1832
+ log.title("Removing context package");
1833
+ const pkg = await getPackage(projectRoot, alias);
1834
+ if (!pkg) {
1835
+ throw new Error(
1836
+ `Package '${alias}' not found in configuration.
1837
+ Run \`nocaap list\` to see installed packages.`
1838
+ );
1839
+ }
1840
+ log.info(`Package: ${alias}`);
1841
+ log.dim(` Source: ${pkg.source}`);
1842
+ if (pkg.path) {
1843
+ log.dim(` Path: ${pkg.path}`);
1844
+ }
1845
+ log.newline();
1846
+ const packagePath = getPackagePath(projectRoot, alias);
1847
+ if (!options.force && await exists(packagePath)) {
1848
+ if (await isGitRepo(packagePath)) {
1849
+ try {
1850
+ if (await isDirty(packagePath)) {
1851
+ log.warn("Package has uncommitted changes.");
1852
+ log.newline();
1853
+ const shouldContinue = await confirm({
1854
+ message: "Remove anyway? Local changes will be lost.",
1855
+ default: false
1856
+ });
1857
+ if (!shouldContinue) {
1858
+ log.info("Removal cancelled.");
1859
+ return;
1860
+ }
1861
+ }
1862
+ } catch {
1863
+ }
1864
+ }
1865
+ }
1866
+ const dirSpinner = createSpinner("Removing package directory...").start();
1867
+ try {
1868
+ if (await exists(packagePath)) {
1869
+ const fs8 = await import('fs-extra');
1870
+ await fs8.default.remove(packagePath);
1871
+ }
1872
+ dirSpinner.succeed("Removed package directory");
1873
+ } catch (error) {
1874
+ dirSpinner.fail("Failed to remove directory");
1875
+ throw error;
1876
+ }
1877
+ const configSpinner = createSpinner("Updating configuration...").start();
1878
+ try {
1879
+ await removePackage(projectRoot, alias);
1880
+ await removeLockEntry(projectRoot, alias);
1881
+ configSpinner.succeed("Configuration updated");
1882
+ } catch (error) {
1883
+ configSpinner.fail("Failed to update configuration");
1884
+ throw error;
1885
+ }
1886
+ log.newline();
1887
+ await generateIndexWithProgress(projectRoot);
1888
+ log.newline();
1889
+ log.success(`Package '${alias}' removed successfully.`);
1890
+ }
1891
+
1892
+ // src/commands/config.ts
1893
+ async function configCommand(key, value, options) {
1894
+ if (options.list || !key && !value) {
1895
+ await showAllConfig();
1896
+ return;
1897
+ }
1898
+ switch (key) {
1899
+ case "registry":
1900
+ await handleRegistryConfig(value, options.clear);
1901
+ break;
1902
+ default:
1903
+ log.error(`Unknown config key: ${style.code(key ?? "")}`);
1904
+ log.newline();
1905
+ log.info("Available keys:");
1906
+ log.dim(" registry - Default registry URL for `nocaap setup`");
1907
+ break;
1908
+ }
1909
+ }
1910
+ async function showAllConfig() {
1911
+ const config = await getGlobalConfig();
1912
+ const configPath = getGlobalConfigPath();
1913
+ log.title("Global Configuration");
1914
+ log.dim(`Location: ${configPath}`);
1915
+ log.newline();
1916
+ if (Object.keys(config).length === 0 || !config.defaultRegistry) {
1917
+ log.info("No configuration set.");
1918
+ log.newline();
1919
+ log.dim("Set your organization's registry:");
1920
+ log.dim(" nocaap config registry <url>");
1921
+ return;
1922
+ }
1923
+ if (config.defaultRegistry) {
1924
+ log.info(`${style.bold("registry")}: ${config.defaultRegistry}`);
1925
+ }
1926
+ if (config.updatedAt) {
1927
+ log.newline();
1928
+ log.dim(`Last updated: ${new Date(config.updatedAt).toLocaleString()}`);
1929
+ }
1930
+ }
1931
+ async function handleRegistryConfig(value, clear) {
1932
+ if (clear) {
1933
+ await clearDefaultRegistry();
1934
+ log.success("Default registry cleared.");
1935
+ return;
1936
+ }
1937
+ if (value) {
1938
+ try {
1939
+ new URL(value);
1940
+ } catch {
1941
+ throw new Error(
1942
+ `Invalid URL: ${value}
1943
+ Please provide a valid URL, e.g.:
1944
+ https://raw.githubusercontent.com/your-org/hub/main/nocaap-registry.json`
1945
+ );
1946
+ }
1947
+ await setDefaultRegistry(value);
1948
+ log.success("Default registry set!");
1949
+ log.newline();
1950
+ log.info(`Registry: ${style.url(value)}`);
1951
+ log.newline();
1952
+ log.dim("Now you can run `nocaap setup` without the --registry flag.");
1953
+ return;
1954
+ }
1955
+ const registry = await getDefaultRegistry();
1956
+ if (registry) {
1957
+ const envVar = process.env.NOCAAP_DEFAULT_REGISTRY;
1958
+ log.info(`Default registry: ${style.url(registry)}`);
1959
+ if (envVar) {
1960
+ log.dim(" (from NOCAAP_DEFAULT_REGISTRY environment variable)");
1961
+ } else {
1962
+ log.dim(" (from global config)");
1963
+ }
1964
+ } else {
1965
+ log.info("No default registry configured.");
1966
+ log.newline();
1967
+ log.dim("Set one with:");
1968
+ log.dim(" nocaap config registry <url>");
1969
+ log.newline();
1970
+ log.dim("Example:");
1971
+ log.dim(
1972
+ " nocaap config registry https://raw.githubusercontent.com/your-org/hub/main/nocaap-registry.json"
1973
+ );
1974
+ }
1975
+ }
1976
+
1977
+ // src/utils/providers.ts
1978
+ function detectProvider(url) {
1979
+ const normalized = url.toLowerCase();
1980
+ if (normalized.includes("github.com") || normalized.includes("github:")) {
1981
+ return "github";
1982
+ }
1983
+ if (normalized.includes("gitlab.com") || normalized.includes("gitlab:")) {
1984
+ return "gitlab";
1985
+ }
1986
+ if (normalized.includes("bitbucket.org") || normalized.includes("bitbucket:")) {
1987
+ return "bitbucket";
1988
+ }
1989
+ return "unknown";
1990
+ }
1991
+ function parseRepoInfo(url) {
1992
+ const provider = detectProvider(url);
1993
+ const cleaned = url.replace(/\.git$/, "");
1994
+ const sshMatch = cleaned.match(/git@[^:]+:([^/]+)\/(.+)/);
1995
+ if (sshMatch && sshMatch[1] && sshMatch[2]) {
1996
+ return {
1997
+ provider,
1998
+ owner: sshMatch[1],
1999
+ repo: sshMatch[2]
2000
+ };
2001
+ }
2002
+ const httpsMatch = cleaned.match(/https?:\/\/[^/]+\/([^/]+)\/(.+)/);
2003
+ if (httpsMatch && httpsMatch[1] && httpsMatch[2]) {
2004
+ return {
2005
+ provider,
2006
+ owner: httpsMatch[1],
2007
+ repo: httpsMatch[2]
2008
+ };
2009
+ }
2010
+ return {
2011
+ provider,
2012
+ owner: "",
2013
+ repo: ""
2014
+ };
2015
+ }
2016
+ function buildNewPrUrl(info, branch) {
2017
+ const { provider, owner, repo } = info;
2018
+ switch (provider) {
2019
+ case "github":
2020
+ return `https://github.com/${owner}/${repo}/compare/main...${branch}?expand=1`;
2021
+ case "gitlab":
2022
+ return `https://gitlab.com/${owner}/${repo}/-/merge_requests/new?merge_request[source_branch]=${branch}`;
2023
+ case "bitbucket":
2024
+ return `https://bitbucket.org/${owner}/${repo}/pull-requests/new?source=${branch}`;
2025
+ default:
2026
+ return `https://github.com/${owner}/${repo}`;
2027
+ }
2028
+ }
2029
+ var execAsync = promisify(exec);
2030
+ async function isGhAvailable() {
2031
+ try {
2032
+ await execAsync("gh auth status");
2033
+ return true;
2034
+ } catch {
2035
+ return false;
2036
+ }
2037
+ }
2038
+ function hasGitHubToken() {
2039
+ return Boolean(process.env.GITHUB_TOKEN);
2040
+ }
2041
+ async function detectPrMethod() {
2042
+ if (await isGhAvailable()) {
2043
+ log.debug("gh CLI available and authenticated");
2044
+ return "gh";
2045
+ }
2046
+ if (hasGitHubToken()) {
2047
+ log.debug("GITHUB_TOKEN available for API");
2048
+ return "api";
2049
+ }
2050
+ log.debug("No automated PR method available, will use manual URL");
2051
+ return "manual";
2052
+ }
2053
+ async function createPrViaGh(repoDir, branch, title, body) {
2054
+ log.debug(`Creating PR via gh CLI in ${repoDir}`);
2055
+ try {
2056
+ const { stdout } = await execAsync(
2057
+ `gh pr create --title "${escapeShell(title)}" --body "${escapeShell(body)}" --head "${branch}"`,
2058
+ { cwd: repoDir }
2059
+ );
2060
+ const url = stdout.trim();
2061
+ log.debug(`PR created via gh: ${url}`);
2062
+ return url;
2063
+ } catch (error) {
2064
+ const message = error instanceof Error ? error.message : "Unknown error";
2065
+ log.debug(`gh pr create failed: ${message}`);
2066
+ return null;
2067
+ }
2068
+ }
2069
+ async function createPrViaApi(owner, repo, branch, baseBranch, title, body) {
2070
+ const token = process.env.GITHUB_TOKEN;
2071
+ if (!token) {
2072
+ log.debug("No GITHUB_TOKEN available for API");
2073
+ return null;
2074
+ }
2075
+ log.debug(`Creating PR via GitHub API for ${owner}/${repo}`);
2076
+ try {
2077
+ const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls`, {
2078
+ method: "POST",
2079
+ headers: {
2080
+ Authorization: `Bearer ${token}`,
2081
+ Accept: "application/vnd.github+json",
2082
+ "Content-Type": "application/json",
2083
+ "X-GitHub-Api-Version": "2022-11-28"
2084
+ },
2085
+ body: JSON.stringify({
2086
+ title,
2087
+ body,
2088
+ head: branch,
2089
+ base: baseBranch
2090
+ })
2091
+ });
2092
+ if (!response.ok) {
2093
+ const errorData = await response.json().catch(() => ({}));
2094
+ log.debug(`GitHub API error: ${response.status} - ${JSON.stringify(errorData)}`);
2095
+ return null;
2096
+ }
2097
+ const data = await response.json();
2098
+ const url = data.html_url || null;
2099
+ if (url) {
2100
+ log.debug(`PR created via API: ${url}`);
2101
+ }
2102
+ return url;
2103
+ } catch (error) {
2104
+ const message = error instanceof Error ? error.message : "Unknown error";
2105
+ log.debug(`GitHub API request failed: ${message}`);
2106
+ return null;
2107
+ }
2108
+ }
2109
+ async function createPr(options) {
2110
+ const method = await detectPrMethod();
2111
+ switch (method) {
2112
+ case "gh": {
2113
+ const url = await createPrViaGh(
2114
+ options.repoDir,
2115
+ options.branch,
2116
+ options.title,
2117
+ options.body
2118
+ );
2119
+ if (url) {
2120
+ return { url, method: "gh", success: true };
2121
+ }
2122
+ log.debug("gh CLI failed, trying GitHub API");
2123
+ if (hasGitHubToken()) {
2124
+ const apiUrl = await createPrViaApi(
2125
+ options.owner,
2126
+ options.repo,
2127
+ options.branch,
2128
+ options.baseBranch,
2129
+ options.title,
2130
+ options.body
2131
+ );
2132
+ if (apiUrl) {
2133
+ return { url: apiUrl, method: "api", success: true };
2134
+ }
2135
+ }
2136
+ return {
2137
+ url: options.manualUrl,
2138
+ method: "manual",
2139
+ success: false,
2140
+ error: "Could not create PR automatically. Please create it manually."
2141
+ };
2142
+ }
2143
+ case "api": {
2144
+ const url = await createPrViaApi(
2145
+ options.owner,
2146
+ options.repo,
2147
+ options.branch,
2148
+ options.baseBranch,
2149
+ options.title,
2150
+ options.body
2151
+ );
2152
+ if (url) {
2153
+ return { url, method: "api", success: true };
2154
+ }
2155
+ return {
2156
+ url: options.manualUrl,
2157
+ method: "manual",
2158
+ success: false,
2159
+ error: "Could not create PR via API. Please create it manually."
2160
+ };
2161
+ }
2162
+ case "manual":
2163
+ default:
2164
+ return {
2165
+ url: options.manualUrl,
2166
+ method: "manual",
2167
+ success: false,
2168
+ error: "No automated PR creation method available."
2169
+ };
2170
+ }
2171
+ }
2172
+ function escapeShell(str) {
2173
+ return str.replace(/"/g, '\\"').replace(/\$/g, "\\$").replace(/`/g, "\\`");
2174
+ }
2175
+
2176
+ // src/commands/push.ts
2177
+ function getDateString() {
2178
+ const now = /* @__PURE__ */ new Date();
2179
+ const year = now.getFullYear();
2180
+ const month = String(now.getMonth() + 1).padStart(2, "0");
2181
+ const day = String(now.getDate()).padStart(2, "0");
2182
+ return `${year}${month}${day}`;
2183
+ }
2184
+ function generateBranchName(alias) {
2185
+ return `nocaap/${alias}-${getDateString()}`;
2186
+ }
2187
+ async function getAllPackages(projectRoot) {
2188
+ const config = await readConfig(projectRoot);
2189
+ const lockfile = await readLockfile(projectRoot);
2190
+ if (!config) {
2191
+ return [];
2192
+ }
2193
+ return config.packages.map((pkg) => ({
2194
+ alias: pkg.alias,
2195
+ source: pkg.source,
2196
+ path: pkg.path,
2197
+ localCommit: lockfile[pkg.alias]?.commitHash || ""
2198
+ }));
2199
+ }
2200
+ async function selectPackagesToPush(packages) {
2201
+ if (packages.length === 0) {
2202
+ return [];
2203
+ }
2204
+ const choices = packages.map((pkg) => ({
2205
+ name: `${pkg.alias} (${pkg.source})`,
2206
+ value: pkg.alias,
2207
+ checked: false
2208
+ }));
2209
+ const selected = await checkbox({
2210
+ message: "Select packages to push:",
2211
+ choices,
2212
+ pageSize: 15
2213
+ });
2214
+ return selected;
2215
+ }
2216
+ async function pushSinglePackage(projectRoot, pkg, commitMessage) {
2217
+ const packagePath = getPackagePath(projectRoot, pkg.alias);
2218
+ const branchName = generateBranchName(pkg.alias);
2219
+ const repoInfo = parseRepoInfo(pkg.source);
2220
+ const checkSpinner = createSpinner("Checking upstream...").start();
2221
+ try {
2222
+ const defaultBranch = await getDefaultBranch(pkg.source);
2223
+ const remoteCommit = await getRemoteCommitHash(pkg.source, defaultBranch);
2224
+ if (remoteCommit !== pkg.localCommit) {
2225
+ checkSpinner.fail("Upstream has diverged");
2226
+ return {
2227
+ success: false,
2228
+ error: `Upstream has changed. Run 'nocaap update ${pkg.alias}' first.`
2229
+ };
2230
+ }
2231
+ checkSpinner.succeed("Upstream in sync");
2232
+ } catch (error) {
2233
+ checkSpinner.fail("Failed to check upstream");
2234
+ const msg = error instanceof Error ? error.message : "Unknown error";
2235
+ return { success: false, error: msg };
2236
+ }
2237
+ const cloneSpinner = createSpinner("Cloning upstream...").start();
2238
+ let tempDir;
2239
+ let cleanup;
2240
+ try {
2241
+ const defaultBranch = await getDefaultBranch(pkg.source);
2242
+ const result = await cloneToTemp(pkg.source, defaultBranch);
2243
+ tempDir = result.tempDir;
2244
+ cleanup = result.cleanup;
2245
+ cloneSpinner.succeed("Cloned to temp directory");
2246
+ } catch (error) {
2247
+ cloneSpinner.fail("Clone failed");
2248
+ const msg = error instanceof Error ? error.message : "Unknown error";
2249
+ return { success: false, error: msg };
2250
+ }
2251
+ try {
2252
+ const branchSpinner = createSpinner("Creating branch...").start();
2253
+ await createBranch(tempDir, branchName);
2254
+ branchSpinner.succeed(`Created branch: ${branchName}`);
2255
+ const copySpinner = createSpinner("Copying changes...").start();
2256
+ const targetPath = pkg.path ? join(tempDir, pkg.path.replace(/^\/+/, "")) : tempDir;
2257
+ await ensureDir(targetPath);
2258
+ const items = await fs2.readdir(packagePath);
2259
+ for (const item of items) {
2260
+ if (item === ".git") continue;
2261
+ const srcPath = join(packagePath, item);
2262
+ const destPath = join(targetPath, item);
2263
+ await fs2.copy(srcPath, destPath, { overwrite: true });
2264
+ }
2265
+ copySpinner.succeed("Changes copied");
2266
+ const commitSpinner = createSpinner("Committing...").start();
2267
+ try {
2268
+ await commitAll(tempDir, commitMessage);
2269
+ commitSpinner.succeed("Changes committed");
2270
+ } catch (error) {
2271
+ const msg = error instanceof Error ? error.message : "";
2272
+ if (msg.includes("nothing to commit")) {
2273
+ commitSpinner.warn("No changes to commit");
2274
+ await cleanup();
2275
+ return { success: true, error: "No changes detected" };
2276
+ }
2277
+ throw error;
2278
+ }
2279
+ const pushSpinner = createSpinner("Pushing to remote...").start();
2280
+ try {
2281
+ await pushBranch(tempDir, branchName);
2282
+ pushSpinner.succeed("Pushed to remote");
2283
+ } catch (error) {
2284
+ pushSpinner.fail("Push failed");
2285
+ throw error;
2286
+ }
2287
+ const prSpinner = createSpinner("Creating PR...").start();
2288
+ const defaultBranch = await getDefaultBranch(pkg.source);
2289
+ const manualUrl = buildNewPrUrl(repoInfo, branchName);
2290
+ const prResult = await createPr({
2291
+ repoDir: tempDir,
2292
+ owner: repoInfo.owner,
2293
+ repo: repoInfo.repo,
2294
+ branch: branchName,
2295
+ baseBranch: defaultBranch,
2296
+ title: `Update ${pkg.alias} context via nocaap`,
2297
+ body: `This PR was created automatically by nocaap.
2298
+
2299
+ **Commit message:** ${commitMessage}`,
2300
+ manualUrl
2301
+ });
2302
+ if (prResult.success) {
2303
+ prSpinner.succeed("PR created");
2304
+ } else {
2305
+ prSpinner.warn("PR not created automatically");
2306
+ }
2307
+ await cleanup();
2308
+ return {
2309
+ success: true,
2310
+ prUrl: prResult.url || manualUrl
2311
+ };
2312
+ } catch (error) {
2313
+ await cleanup();
2314
+ const msg = error instanceof Error ? error.message : "Unknown error";
2315
+ return { success: false, error: msg };
2316
+ }
2317
+ }
2318
+ async function pushCommand(alias, options) {
2319
+ const projectRoot = process.cwd();
2320
+ log.title("nocaap Push");
2321
+ log.newline();
2322
+ const allPackages = await getAllPackages(projectRoot);
2323
+ if (allPackages.length === 0) {
2324
+ log.error("No packages configured. Run `nocaap setup` or `nocaap add` first.");
2325
+ return;
2326
+ }
2327
+ let packagesToPush;
2328
+ if (options.all) {
2329
+ packagesToPush = allPackages;
2330
+ log.info(`Pushing all ${packagesToPush.length} package(s)...`);
2331
+ } else if (alias) {
2332
+ const pkg = allPackages.find((p) => p.alias === alias);
2333
+ if (!pkg) {
2334
+ log.error(`Package '${alias}' not found in config.`);
2335
+ log.dim("Available packages:");
2336
+ for (const p of allPackages) {
2337
+ log.dim(` - ${p.alias}`);
2338
+ }
2339
+ return;
2340
+ }
2341
+ packagesToPush = [pkg];
2342
+ } else {
2343
+ log.info("Select packages to push:");
2344
+ log.newline();
2345
+ const selectedAliases = await selectPackagesToPush(allPackages);
2346
+ if (selectedAliases.length === 0) {
2347
+ log.warn("No packages selected. Push cancelled.");
2348
+ return;
2349
+ }
2350
+ packagesToPush = allPackages.filter((p) => selectedAliases.includes(p.alias));
2351
+ }
2352
+ log.newline();
2353
+ const defaultMessage = packagesToPush.length === 1 && packagesToPush[0] ? `Update ${packagesToPush[0].alias} context via nocaap` : "Update context via nocaap";
2354
+ const commitMessage = options.message || defaultMessage;
2355
+ const results = [];
2356
+ for (const pkg of packagesToPush) {
2357
+ log.hr();
2358
+ log.newline();
2359
+ log.info(`Pushing ${style.bold(pkg.alias)}...`);
2360
+ log.dim(` Source: ${pkg.source}`);
2361
+ if (pkg.path) {
2362
+ log.dim(` Path: ${pkg.path}`);
2363
+ }
2364
+ log.newline();
2365
+ const result = await pushSinglePackage(projectRoot, pkg, commitMessage);
2366
+ results.push({ alias: pkg.alias, ...result });
2367
+ if (result.success && result.prUrl) {
2368
+ log.newline();
2369
+ log.success(`PR created for ${pkg.alias}:`);
2370
+ log.info(` ${style.url(result.prUrl)}`);
2371
+ } else if (result.error) {
2372
+ log.newline();
2373
+ log.error(`Failed: ${result.error}`);
2374
+ }
2375
+ }
2376
+ log.newline();
2377
+ log.hr();
2378
+ log.newline();
2379
+ const successCount = results.filter((r) => r.success).length;
2380
+ const failCount = results.filter((r) => !r.success).length;
2381
+ if (successCount > 0) {
2382
+ log.success(`${successCount} package(s) pushed successfully.`);
2383
+ const withPrs = results.filter((r) => r.success && r.prUrl);
2384
+ if (withPrs.length > 0) {
2385
+ log.newline();
2386
+ log.info("Pull Requests:");
2387
+ for (const r of withPrs) {
2388
+ log.dim(` ${r.alias}: ${r.prUrl}`);
2389
+ }
2390
+ }
2391
+ }
2392
+ if (failCount > 0) {
2393
+ log.newline();
2394
+ log.warn(`${failCount} package(s) failed.`);
2395
+ for (const r of results.filter((r2) => !r2.success)) {
2396
+ log.dim(` ${r.alias}: ${r.error}`);
2397
+ }
2398
+ }
2399
+ }
2400
+
2401
+ // src/index.ts
2402
+ var program = new Command();
2403
+ program.name("nocaap").description(
2404
+ "Normalized Organizational Context-as-a-Package\n\nStandardize your AI agent context across teams.\nRun `nocaap setup` to get started."
2405
+ ).version("0.1.0");
2406
+ program.command("setup").description("Interactive setup wizard to configure context packages").option("-r, --registry <url>", "Registry URL to fetch contexts from").action(async (options) => {
2407
+ try {
2408
+ await setupCommand({ registry: options.registry });
2409
+ } catch (error) {
2410
+ const message = error instanceof Error ? error.message : String(error);
2411
+ log.error(message);
2412
+ process.exit(1);
2413
+ }
2414
+ });
2415
+ program.command("add <repo>").description("Add a context package from a Git repository").option("-p, --path <path>", "Sparse checkout path within the repo").option("-a, --alias <name>", "Local alias for the package").option("-b, --branch <branch>", "Branch or tag to checkout", "main").action(async (repo, options) => {
2416
+ try {
2417
+ await addCommand(repo, {
2418
+ path: options.path,
2419
+ alias: options.alias,
2420
+ branch: options.branch
2421
+ });
2422
+ } catch (error) {
2423
+ const message = error instanceof Error ? error.message : String(error);
2424
+ log.error(message);
2425
+ process.exit(1);
2426
+ }
2427
+ });
2428
+ program.command("update [alias]").description("Update context packages and regenerate index").option("--force", "Force update even if clean").action(async (alias, options) => {
2429
+ try {
2430
+ await updateCommand(alias, { force: options.force });
2431
+ } catch (error) {
2432
+ const message = error instanceof Error ? error.message : String(error);
2433
+ log.error(message);
2434
+ process.exit(1);
2435
+ }
2436
+ });
2437
+ program.command("list").alias("ls").description("List installed context packages").action(async () => {
2438
+ try {
2439
+ await listCommand();
2440
+ } catch (error) {
2441
+ const message = error instanceof Error ? error.message : String(error);
2442
+ log.error(message);
2443
+ process.exit(1);
2444
+ }
2445
+ });
2446
+ program.command("remove <alias>").alias("rm").description("Remove a context package").option("--force", "Force removal even if dirty").action(async (alias, options) => {
2447
+ try {
2448
+ await removeCommand(alias, { force: options.force });
2449
+ } catch (error) {
2450
+ const message = error instanceof Error ? error.message : String(error);
2451
+ log.error(message);
2452
+ process.exit(1);
2453
+ }
2454
+ });
2455
+ program.command("config [key] [value]").description("Manage global nocaap configuration").option("-l, --list", "Show all configuration").option("--clear", "Clear the specified config key").action(async (key, value, options) => {
2456
+ try {
2457
+ await configCommand(key, value, {
2458
+ list: options.list,
2459
+ clear: options.clear
2460
+ });
2461
+ } catch (error) {
2462
+ const message = error instanceof Error ? error.message : String(error);
2463
+ log.error(message);
2464
+ process.exit(1);
2465
+ }
2466
+ });
2467
+ program.command("push [alias]").description("Push local changes to upstream as a PR").option("-m, --message <message>", "Commit message").option("-a, --all", "Push all packages with changes").action(async (alias, options) => {
2468
+ try {
2469
+ await pushCommand(alias, {
2470
+ message: options.message,
2471
+ all: options.all
2472
+ });
2473
+ } catch (error) {
2474
+ const message = error instanceof Error ? error.message : String(error);
2475
+ log.error(message);
2476
+ process.exit(1);
2477
+ }
2478
+ });
2479
+ program.command("generate").alias("index").description("Regenerate INDEX.md without updating packages").action(async () => {
2480
+ try {
2481
+ const projectRoot = process.cwd();
2482
+ await generateIndexWithProgress(projectRoot);
2483
+ } catch (error) {
2484
+ const message = error instanceof Error ? error.message : String(error);
2485
+ log.error(message);
2486
+ process.exit(1);
2487
+ }
2488
+ });
2489
+ program.parse();
2490
+ //# sourceMappingURL=index.js.map
2491
+ //# sourceMappingURL=index.js.map