srcpack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/init.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function runInit(): Promise<void>;
@@ -0,0 +1,37 @@
1
+ import type { BundleConfigInput } from "./config.ts";
2
+ export interface FileEntry {
3
+ path: string;
4
+ lines: number;
5
+ startLine: number;
6
+ endLine: number;
7
+ }
8
+ export interface BundleResult {
9
+ content: string;
10
+ index: FileEntry[];
11
+ }
12
+ /**
13
+ * Resolve bundle config to a list of file paths.
14
+ * Respects .gitignore patterns in the working directory.
15
+ */
16
+ export declare function resolvePatterns(config: BundleConfigInput, cwd: string): Promise<string[]>;
17
+ /**
18
+ * Format the index header block.
19
+ * Format designed for LLM context files (ChatGPT, Grok, Gemini):
20
+ * - Numbered entries for cross-reference with file separators
21
+ * - ASCII-only characters for broad compatibility
22
+ * - Line locations that point to actual file content
23
+ */
24
+ export declare function formatIndex(index: FileEntry[]): string;
25
+ export interface BundleOptions {
26
+ includeIndex?: boolean;
27
+ }
28
+ /**
29
+ * Create a bundle from a list of files.
30
+ * Line numbers in the index point to the first line of actual file content,
31
+ * not to the separator line.
32
+ */
33
+ export declare function createBundle(files: string[], cwd: string, options?: BundleOptions): Promise<BundleResult>;
34
+ /**
35
+ * Bundle a single named bundle from config
36
+ */
37
+ export declare function bundleOne(name: string, config: BundleConfigInput, cwd: string): Promise<BundleResult>;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ export declare function expandPath(p: string): string;
3
+ declare const BundleConfigSchema: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodObject<{
4
+ include: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
5
+ outfile: z.ZodOptional<z.ZodString>;
6
+ index: z.ZodDefault<z.ZodBoolean>;
7
+ }, z.core.$strip>]>;
8
+ declare const UploadConfigSchema: z.ZodObject<{
9
+ provider: z.ZodLiteral<"gdrive">;
10
+ folder: z.ZodOptional<z.ZodString>;
11
+ clientId: z.ZodString;
12
+ clientSecret: z.ZodString;
13
+ }, z.core.$strip>;
14
+ declare const ConfigSchema: z.ZodObject<{
15
+ outDir: z.ZodDefault<z.ZodString>;
16
+ upload: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
17
+ provider: z.ZodLiteral<"gdrive">;
18
+ folder: z.ZodOptional<z.ZodString>;
19
+ clientId: z.ZodString;
20
+ clientSecret: z.ZodString;
21
+ }, z.core.$strip>, z.ZodArray<z.ZodObject<{
22
+ provider: z.ZodLiteral<"gdrive">;
23
+ folder: z.ZodOptional<z.ZodString>;
24
+ clientId: z.ZodString;
25
+ clientSecret: z.ZodString;
26
+ }, z.core.$strip>>]>>;
27
+ bundles: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodObject<{
28
+ include: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
29
+ outfile: z.ZodOptional<z.ZodString>;
30
+ index: z.ZodDefault<z.ZodBoolean>;
31
+ }, z.core.$strip>]>>;
32
+ }, z.core.$strip>;
33
+ export type UploadConfig = z.infer<typeof UploadConfigSchema>;
34
+ export type BundleConfig = z.infer<typeof BundleConfigSchema>;
35
+ export type BundleConfigInput = z.input<typeof BundleConfigSchema>;
36
+ export type Config = z.infer<typeof ConfigSchema>;
37
+ export type ConfigInput = z.input<typeof ConfigSchema>;
38
+ export declare function defineConfig(config: ConfigInput): ConfigInput;
39
+ export declare class ConfigError extends Error {
40
+ constructor(message: string);
41
+ }
42
+ export declare function parseConfig(value: unknown): Config;
43
+ export declare function loadConfig(searchFrom?: string): Promise<Config | null>;
44
+ export declare function loadConfigFromFile(filepath: string): Promise<Config | null>;
45
+ export {};
@@ -0,0 +1,33 @@
1
+ import { OAuthError } from "oauth-callback";
2
+ import type { UploadConfig } from "./config.ts";
3
+ export interface Tokens {
4
+ access_token: string;
5
+ refresh_token?: string;
6
+ expires_at?: number;
7
+ token_type: string;
8
+ scope: string;
9
+ }
10
+ /**
11
+ * Loads stored tokens from disk.
12
+ * Returns null if no tokens exist or they cannot be read.
13
+ */
14
+ export declare function loadTokens(): Promise<Tokens | null>;
15
+ /**
16
+ * Removes stored tokens from disk.
17
+ */
18
+ export declare function clearTokens(): Promise<void>;
19
+ /**
20
+ * Gets valid tokens, refreshing if necessary.
21
+ * Returns null if no tokens exist or refresh fails.
22
+ */
23
+ export declare function getValidTokens(config: UploadConfig): Promise<Tokens | null>;
24
+ /**
25
+ * Performs OAuth login flow - opens browser for user consent.
26
+ * Stores tokens on success for future use.
27
+ */
28
+ export declare function login(config: UploadConfig): Promise<Tokens>;
29
+ /**
30
+ * Ensures we have valid tokens - loads existing or triggers login.
31
+ */
32
+ export declare function ensureAuthenticated(config: UploadConfig): Promise<Tokens>;
33
+ export { OAuthError };
@@ -0,0 +1,2 @@
1
+ export { defineConfig, loadConfig, loadConfigFromFile } from "./config.ts";
2
+ export type { Config, BundleConfig } from "./config.ts";
@@ -0,0 +1 @@
1
+ export declare function runInit(): Promise<void>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "srcpack",
3
+ "version": "0.1.0",
4
+ "description": "Zero-config CLI for bundling code into LLM-optimized context files",
5
+ "keywords": [
6
+ "llm",
7
+ "ai",
8
+ "context",
9
+ "chatgpt",
10
+ "claude",
11
+ "gemini",
12
+ "grok",
13
+ "copilot",
14
+ "code-bundler",
15
+ "concatenate",
16
+ "codebase",
17
+ "source-code",
18
+ "monorepo",
19
+ "prompt",
20
+ "context-window",
21
+ "rag",
22
+ "embedding",
23
+ "ai-tools",
24
+ "ai-coding",
25
+ "agentic",
26
+ "glob",
27
+ "cli",
28
+ "bun"
29
+ ],
30
+ "author": "Konstantin Tarkus <koistya@kriasoft.com>",
31
+ "license": "MIT",
32
+ "homepage": "https://kriasoft.com/srcpack/",
33
+ "repository": "github:kriasoft/srcpack",
34
+ "type": "module",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "default": "./dist/index.js"
39
+ }
40
+ },
41
+ "bin": {
42
+ "srcpack": "./dist/cli.js"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "src",
47
+ "schema.json"
48
+ ],
49
+ "scripts": {
50
+ "build": "bun build ./src/cli.ts ./src/index.ts --outdir ./dist --target bun && tsc",
51
+ "typecheck": "tsc -p tsconfig.check.json",
52
+ "check": "tsc -p tsconfig.check.json",
53
+ "test": "bun test",
54
+ "test:watch": "bun test --watch",
55
+ "docs:dev": "vitepress dev",
56
+ "docs:build": "vitepress build",
57
+ "docs:preview": "vitepress preview",
58
+ "docs:deploy": "gh-pages -d .vitepress/dist"
59
+ },
60
+ "dependencies": {
61
+ "@clack/prompts": "^0.11.0",
62
+ "cosmiconfig": "^9.0.0",
63
+ "ignore": "^7.0.5",
64
+ "oauth-callback": "^1.2.1",
65
+ "zod": "^4.3.5"
66
+ },
67
+ "devDependencies": {
68
+ "@types/bun": "^1.3.6",
69
+ "gh-pages": "^6.3.0",
70
+ "prettier": "^3.8.0",
71
+ "typescript": "^5.9.3",
72
+ "vitepress": "^2.0.0-alpha.15"
73
+ }
74
+ }
package/src/bundle.ts ADDED
@@ -0,0 +1,237 @@
1
+ // SPDX-License-Identifier: MIT
2
+
3
+ import { join } from "node:path";
4
+ import { Glob } from "bun";
5
+ import ignore, { type Ignore } from "ignore";
6
+ import type { BundleConfigInput } from "./config.ts";
7
+
8
+ export interface FileEntry {
9
+ path: string; // Relative path from cwd
10
+ lines: number; // Line count in source file
11
+ startLine: number; // Start line in bundle (1-indexed)
12
+ endLine: number; // End line in bundle
13
+ }
14
+
15
+ export interface BundleResult {
16
+ content: string;
17
+ index: FileEntry[];
18
+ }
19
+
20
+ /**
21
+ * Normalize BundleConfig to arrays of include/exclude patterns
22
+ */
23
+ function normalizePatterns(config: BundleConfigInput): {
24
+ include: string[];
25
+ exclude: string[];
26
+ } {
27
+ let patterns: string[];
28
+
29
+ if (typeof config === "string") {
30
+ patterns = [config];
31
+ } else if (Array.isArray(config)) {
32
+ patterns = config;
33
+ } else {
34
+ patterns = Array.isArray(config.include)
35
+ ? config.include
36
+ : [config.include];
37
+ }
38
+
39
+ const include: string[] = [];
40
+ const exclude: string[] = [];
41
+
42
+ for (const p of patterns) {
43
+ if (p.startsWith("!")) {
44
+ exclude.push(p.slice(1));
45
+ } else {
46
+ include.push(p);
47
+ }
48
+ }
49
+
50
+ return { include, exclude };
51
+ }
52
+
53
+ /**
54
+ * Check if a path matches any of the exclusion patterns
55
+ */
56
+ function isExcluded(filePath: string, excludePatterns: string[]): boolean {
57
+ for (const pattern of excludePatterns) {
58
+ const glob = new Glob(pattern);
59
+ if (glob.match(filePath)) {
60
+ return true;
61
+ }
62
+ }
63
+ return false;
64
+ }
65
+
66
+ /**
67
+ * Load and parse .gitignore file from a directory
68
+ */
69
+ async function loadGitignore(cwd: string): Promise<Ignore> {
70
+ const ig = ignore();
71
+ const gitignorePath = join(cwd, ".gitignore");
72
+
73
+ try {
74
+ const content = await Bun.file(gitignorePath).text();
75
+ ig.add(content);
76
+ } catch {
77
+ // No .gitignore file, return empty ignore instance
78
+ }
79
+
80
+ return ig;
81
+ }
82
+
83
+ /**
84
+ * Resolve bundle config to a list of file paths.
85
+ * Respects .gitignore patterns in the working directory.
86
+ */
87
+ export async function resolvePatterns(
88
+ config: BundleConfigInput,
89
+ cwd: string,
90
+ ): Promise<string[]> {
91
+ const { include, exclude } = normalizePatterns(config);
92
+ const gitignore = await loadGitignore(cwd);
93
+ const files = new Set<string>();
94
+
95
+ for (const pattern of include) {
96
+ const glob = new Glob(pattern);
97
+ for await (const match of glob.scan({ cwd, onlyFiles: true })) {
98
+ if (!isExcluded(match, exclude) && !gitignore.ignores(match)) {
99
+ files.add(match);
100
+ }
101
+ }
102
+ }
103
+
104
+ // Sort for deterministic output
105
+ return [...files].sort();
106
+ }
107
+
108
+ /**
109
+ * Count lines in a string (handles empty strings correctly)
110
+ */
111
+ function countLines(content: string): number {
112
+ if (content === "") return 0;
113
+ // Count newlines and add 1 for the last line (if not ending with newline)
114
+ const newlines = (content.match(/\n/g) || []).length;
115
+ return content.endsWith("\n") ? newlines : newlines + 1;
116
+ }
117
+
118
+ /**
119
+ * Format the index header block.
120
+ * Format designed for LLM context files (ChatGPT, Grok, Gemini):
121
+ * - Numbered entries for cross-reference with file separators
122
+ * - ASCII-only characters for broad compatibility
123
+ * - Line locations that point to actual file content
124
+ */
125
+ export function formatIndex(index: FileEntry[]): string {
126
+ if (index.length === 0) return "# Index\n# (empty)";
127
+
128
+ const count = index.length;
129
+ const lines = [`# Index (${count} file${count === 1 ? "" : "s"})`];
130
+ for (let i = 0; i < index.length; i++) {
131
+ const entry = index[i]!;
132
+ const num = `[${i + 1}]`.padEnd(5);
133
+ const lineWord = entry.lines === 1 ? "line" : "lines";
134
+ lines.push(
135
+ `# ${num} ${entry.path} L${entry.startLine}-L${entry.endLine} (${entry.lines} ${lineWord})`,
136
+ );
137
+ }
138
+ return lines.join("\n");
139
+ }
140
+
141
+ export interface BundleOptions {
142
+ includeIndex?: boolean; // Default: true
143
+ }
144
+
145
+ /**
146
+ * Format a file separator line with index number for cross-reference.
147
+ * Uses `==>` / `<==` pattern (from Unix head/tail) which is unlikely
148
+ * to appear naturally in bundled files.
149
+ */
150
+ function formatSeparator(index: number, filePath: string): string {
151
+ return `#==> [${index}] ${filePath} <==`;
152
+ }
153
+
154
+ /**
155
+ * Create a bundle from a list of files.
156
+ * Line numbers in the index point to the first line of actual file content,
157
+ * not to the separator line.
158
+ */
159
+ export async function createBundle(
160
+ files: string[],
161
+ cwd: string,
162
+ options: BundleOptions = {},
163
+ ): Promise<BundleResult> {
164
+ const { includeIndex = true } = options;
165
+ const index: FileEntry[] = [];
166
+ const contentParts: string[] = [];
167
+ let currentLine = 1;
168
+
169
+ for (let i = 0; i < files.length; i++) {
170
+ const filePath = files[i]!;
171
+ const fullPath = join(cwd, filePath);
172
+ const content = await Bun.file(fullPath).text();
173
+ const lines = countLines(content);
174
+
175
+ // Separator takes 1 line, then content starts on next line
176
+ const contentStartLine = currentLine + 1;
177
+
178
+ const entry: FileEntry = {
179
+ path: filePath,
180
+ lines,
181
+ startLine: contentStartLine,
182
+ endLine: contentStartLine + Math.max(0, lines - 1),
183
+ };
184
+ index.push(entry);
185
+
186
+ contentParts.push(formatSeparator(i + 1, filePath));
187
+ contentParts.push(content.endsWith("\n") ? content.slice(0, -1) : content);
188
+
189
+ // Next separator line = after content
190
+ currentLine = entry.endLine + 1;
191
+ }
192
+
193
+ if (includeIndex) {
194
+ // Adjust line numbers to account for index header
195
+ // Header: "# Index (N files)" + N index lines + 1 blank line
196
+ const headerLines = index.length + 2;
197
+ for (const entry of index) {
198
+ entry.startLine += headerLines;
199
+ entry.endLine += headerLines;
200
+ }
201
+
202
+ const indexBlock = formatIndex(index);
203
+ const content =
204
+ index.length === 0
205
+ ? indexBlock
206
+ : indexBlock + "\n\n" + contentParts.join("\n");
207
+
208
+ return { content, index };
209
+ }
210
+
211
+ // No index: just join file content
212
+ const content = contentParts.join("\n");
213
+ return { content, index };
214
+ }
215
+
216
+ /**
217
+ * Extract the index option from bundle config (default: true)
218
+ */
219
+ function getIncludeIndex(config: BundleConfigInput): boolean {
220
+ if (typeof config === "object" && !Array.isArray(config)) {
221
+ return config.index ?? true;
222
+ }
223
+ return true;
224
+ }
225
+
226
+ /**
227
+ * Bundle a single named bundle from config
228
+ */
229
+ export async function bundleOne(
230
+ name: string,
231
+ config: BundleConfigInput,
232
+ cwd: string,
233
+ ): Promise<BundleResult> {
234
+ const files = await resolvePatterns(config, cwd);
235
+ const includeIndex = getIncludeIndex(config);
236
+ return createBundle(files, cwd, { includeIndex });
237
+ }