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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/bundle.d.ts +37 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +189620 -0
- package/dist/config.d.ts +45 -0
- package/dist/gdrive.d.ts +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +187079 -0
- package/dist/init.d.ts +1 -0
- package/dist/src/bundle.d.ts +37 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/config.d.ts +45 -0
- package/dist/src/gdrive.d.ts +33 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/init.d.ts +1 -0
- package/dist/tests/bundle.test.d.ts +1 -0
- package/dist/tests/cli.test.d.ts +1 -0
- package/dist/tests/config.test.d.ts +1 -0
- package/package.json +74 -0
- package/src/bundle.ts +237 -0
- package/src/cli.ts +266 -0
- package/src/config.ts +107 -0
- package/src/gdrive.ts +200 -0
- package/src/index.ts +4 -0
- package/src/init.ts +144 -0
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,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 @@
|
|
|
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
|
+
}
|