srcpack 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -48,11 +48,14 @@ Or add to `package.json`:
48
48
 
49
49
  ### Options
50
50
 
51
- | Option | Default | Description |
52
- | --------- | ---------- | -------------------------------- |
53
- | `outDir` | `.srcpack` | Output directory for bundles |
54
- | `bundles` | | Named bundles with glob patterns |
55
- | `upload` | — | Upload destination(s) |
51
+ | Option | Default | Description |
52
+ | ------------- | ---------- | -------------------------------------- |
53
+ | `outDir` | `.srcpack` | Output directory for bundles |
54
+ | `emptyOutDir` | `true`\* | Empty output directory before bundling |
55
+ | `bundles` | — | Named bundles with glob patterns |
56
+ | `upload` | — | Upload destination(s) |
57
+
58
+ \*`emptyOutDir` defaults to `true` when `outDir` is inside project root. When `outDir` is outside root, a warning is emitted unless explicitly set.
56
59
 
57
60
  ### Bundle Config
58
61
 
@@ -90,6 +93,7 @@ export default defineConfig({
90
93
  folderId: "1ABC...", // Google Drive folder ID (from URL)
91
94
  clientId: "...",
92
95
  clientSecret: "...",
96
+ exclude: ["local"], // skip specific bundles
93
97
  },
94
98
  });
95
99
  ```
@@ -127,12 +131,14 @@ export function utils() {
127
131
  ## CLI
128
132
 
129
133
  ```bash
130
- npx srcpack # Bundle all, upload if configured
131
- npx srcpack web api # Bundle specific bundles only
132
- npx srcpack --dry-run # Preview without writing files
133
- npx srcpack --no-upload # Bundle only, skip upload
134
- npx srcpack init # Interactive config setup
135
- npx srcpack login # Authenticate with Google Drive
134
+ npx srcpack # Bundle all, upload if configured
135
+ npx srcpack web api # Bundle specific bundles only
136
+ npx srcpack --dry-run # Preview without writing files
137
+ npx srcpack --emptyOutDir # Empty output directory before bundling
138
+ npx srcpack --no-emptyOutDir # Keep existing files in output directory
139
+ npx srcpack --no-upload # Bundle only, skip upload
140
+ npx srcpack init # Interactive config setup
141
+ npx srcpack login # Authenticate with Google Drive
136
142
  ```
137
143
 
138
144
  ## API
@@ -159,6 +165,8 @@ const config = await loadConfig();
159
165
  - [Discord](https://discord.com/invite/aG83xEb6RX) — Questions, feedback, and discussion
160
166
  - [GitHub Issues](https://github.com/kriasoft/srcpack/issues) — Bug reports and feature requests
161
167
 
168
+ New contributors and OSS maintainers are welcome — join us on Discord or open an issue / PR.
169
+
162
170
  ## Backers
163
171
 
164
172
  <a href="https://reactstarter.com/b/1"><img src="https://reactstarter.com/b/1.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/2"><img src="https://reactstarter.com/b/2.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/3"><img src="https://reactstarter.com/b/3.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/4"><img src="https://reactstarter.com/b/4.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/5"><img src="https://reactstarter.com/b/5.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/6"><img src="https://reactstarter.com/b/6.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/7"><img src="https://reactstarter.com/b/7.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/8"><img src="https://reactstarter.com/b/8.png" height="60" /></a>
package/dist/cli.js CHANGED
@@ -204450,8 +204450,8 @@ var require_src8 = __commonJS((exports, module) => {
204450
204450
  });
204451
204451
 
204452
204452
  // src/cli.ts
204453
- import { mkdir as mkdir3, writeFile as writeFile4 } from "node:fs/promises";
204454
- import { dirname as dirname3, join as join6 } from "node:path";
204453
+ import { mkdir as mkdir3, readdir, rm, writeFile as writeFile4 } from "node:fs/promises";
204454
+ import { dirname as dirname3, isAbsolute, join as join6, relative, resolve } from "node:path";
204455
204455
 
204456
204456
  // node_modules/ora/index.js
204457
204457
  import process8 from "node:process";
@@ -207577,22 +207577,52 @@ function normalizePatterns(config) {
207577
207577
  function isExcluded(filePath, matchers) {
207578
207578
  return matchers.some((match) => match(filePath));
207579
207579
  }
207580
+ function gitignoreToGlobPatterns(lines) {
207581
+ const hasNegation = lines.some((line) => {
207582
+ const trimmed = line.trim();
207583
+ return trimmed.startsWith("!");
207584
+ });
207585
+ if (hasNegation)
207586
+ return [];
207587
+ const patterns = [];
207588
+ for (const line of lines) {
207589
+ const trimmed = line.trim();
207590
+ if (!trimmed || trimmed.startsWith("#"))
207591
+ continue;
207592
+ if (trimmed.startsWith("/") || trimmed.includes("*") || trimmed.includes("?") || trimmed.includes("[") || trimmed.includes("/") || trimmed.includes("\\")) {
207593
+ continue;
207594
+ }
207595
+ const name = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
207596
+ if (name && /^[\w.-]+$/.test(name)) {
207597
+ patterns.push(`**/${name}/**`);
207598
+ }
207599
+ }
207600
+ return patterns;
207601
+ }
207580
207602
  async function loadGitignore(cwd) {
207581
207603
  const ig = import_ignore.default();
207582
207604
  const gitignorePath = join(cwd, ".gitignore");
207605
+ let globPatterns = [];
207583
207606
  try {
207584
207607
  const content = await readFile(gitignorePath, "utf-8");
207585
207608
  ig.add(content);
207609
+ globPatterns = gitignoreToGlobPatterns(content.split(`
207610
+ `));
207586
207611
  } catch {}
207587
- return ig;
207612
+ return { ignore: ig, globPatterns };
207588
207613
  }
207589
207614
  async function resolvePatterns(config, cwd) {
207590
207615
  const { include, exclude, force } = normalizePatterns(config);
207591
207616
  const excludeMatchers = exclude.map((p) => import_picomatch.default(p));
207592
- const gitignore = await loadGitignore(cwd);
207617
+ const { ignore: gitignore, globPatterns } = await loadGitignore(cwd);
207593
207618
  const files = new Set;
207594
207619
  if (include.length > 0) {
207595
- const matches = await import_fast_glob.glob(include, { cwd, onlyFiles: true, dot: true });
207620
+ const matches = await import_fast_glob.glob(include, {
207621
+ cwd,
207622
+ onlyFiles: true,
207623
+ dot: true,
207624
+ ignore: globPatterns
207625
+ });
207596
207626
  for (const match of matches) {
207597
207627
  if (!isExcluded(match, excludeMatchers) && !gitignore.ignores(match)) {
207598
207628
  const fullPath = join(cwd, match);
@@ -221253,10 +221283,12 @@ var UploadConfigSchema = exports_external.object({
221253
221283
  provider: exports_external.literal("gdrive"),
221254
221284
  folderId: exports_external.string().optional(),
221255
221285
  clientId: exports_external.string().min(1),
221256
- clientSecret: exports_external.string().min(1)
221286
+ clientSecret: exports_external.string().min(1),
221287
+ exclude: exports_external.array(exports_external.string()).optional()
221257
221288
  });
221258
221289
  var ConfigSchema = exports_external.object({
221259
221290
  outDir: exports_external.string().default(".srcpack"),
221291
+ emptyOutDir: exports_external.boolean().optional(),
221260
221292
  upload: exports_external.union([UploadConfigSchema, exports_external.array(UploadConfigSchema).min(1)]).optional(),
221261
221293
  bundles: exports_external.record(exports_external.string(), BundleConfigSchema)
221262
221294
  });
@@ -223363,6 +223395,21 @@ function formatNumber(n) {
223363
223395
  function plural(n, singular, pluralForm) {
223364
223396
  return n === 1 ? singular : pluralForm ?? singular + "s";
223365
223397
  }
223398
+ function isOutDirInsideRoot(outDir, root) {
223399
+ const absoluteOutDir = isAbsolute(outDir) ? outDir : resolve(root, outDir);
223400
+ const rel = relative(root, absoluteOutDir);
223401
+ return !rel.startsWith("..") && !isAbsolute(rel);
223402
+ }
223403
+ async function emptyDirectory(dir, skip = []) {
223404
+ let entries;
223405
+ try {
223406
+ entries = await readdir(dir);
223407
+ } catch {
223408
+ return;
223409
+ }
223410
+ const skipSet = new Set(skip);
223411
+ await Promise.all(entries.filter((entry) => !skipSet.has(entry)).map((entry) => rm(join6(dir, entry), { recursive: true, force: true })));
223412
+ }
223366
223413
  async function main() {
223367
223414
  const args = process.argv.slice(2);
223368
223415
  if (args.includes("--help") || args.includes("-h")) {
@@ -223378,9 +223425,11 @@ Usage:
223378
223425
  npx srcpack login Authenticate with Google Drive
223379
223426
 
223380
223427
  Options:
223381
- --dry-run Preview bundles without writing files
223382
- --no-upload Skip uploading to cloud storage
223383
- -h, --help Show this help message
223428
+ --dry-run Preview bundles without writing files
223429
+ --emptyOutDir Empty output directory before bundling
223430
+ --no-emptyOutDir Keep existing files in output directory
223431
+ --no-upload Skip uploading to cloud storage
223432
+ -h, --help Show this help message
223384
223433
  `);
223385
223434
  return;
223386
223435
  }
@@ -223394,6 +223443,7 @@ Options:
223394
223443
  }
223395
223444
  const dryRun = args.includes("--dry-run");
223396
223445
  const noUpload = args.includes("--no-upload");
223446
+ const emptyOutDirFlag = args.includes("--emptyOutDir") ? true : args.includes("--no-emptyOutDir") ? false : undefined;
223397
223447
  const subcommands = ["init", "login"];
223398
223448
  const requestedBundles = args.filter((arg) => !arg.startsWith("-") && !subcommands.includes(arg));
223399
223449
  const config2 = await loadConfig();
@@ -223413,6 +223463,15 @@ Options:
223413
223463
  return;
223414
223464
  }
223415
223465
  const cwd = process.cwd();
223466
+ const outDirInsideRoot = isOutDirInsideRoot(config2.outDir, cwd);
223467
+ const emptyOutDir = emptyOutDirFlag ?? config2.emptyOutDir ?? outDirInsideRoot;
223468
+ if (!outDirInsideRoot && emptyOutDirFlag === undefined && config2.emptyOutDir === undefined) {
223469
+ console.warn(`Warning: outDir "${config2.outDir}" is outside project root. ` + "Use --emptyOutDir to suppress this warning and empty the directory.");
223470
+ }
223471
+ if (emptyOutDir && !dryRun) {
223472
+ const outDirPath = isAbsolute(config2.outDir) ? config2.outDir : resolve(cwd, config2.outDir);
223473
+ await emptyDirectory(outDirPath, [".git"]);
223474
+ }
223416
223475
  const outputs = [];
223417
223476
  const bundleSpinner = ora({
223418
223477
  text: `Bundling ${bundleNames[0]}...`,
@@ -223525,6 +223584,13 @@ function printUploadConfigHelp() {
223525
223584
  `);
223526
223585
  }
223527
223586
  async function handleGdriveUpload(uploadConfig, outputs, cwd) {
223587
+ const excludeSet = new Set(uploadConfig.exclude ?? []);
223588
+ const toUpload = outputs.filter((o2) => !excludeSet.has(o2.name));
223589
+ if (toUpload.length === 0) {
223590
+ console.log(`
223591
+ No bundles to upload (all excluded).`);
223592
+ return;
223593
+ }
223528
223594
  try {
223529
223595
  await ensureAuthenticated(uploadConfig);
223530
223596
  const uploadSpinner = ora({
@@ -223532,10 +223598,10 @@ async function handleGdriveUpload(uploadConfig, outputs, cwd) {
223532
223598
  color: "cyan"
223533
223599
  }).start();
223534
223600
  const results = [];
223535
- for (let i2 = 0;i2 < outputs.length; i2++) {
223536
- const output = outputs[i2];
223601
+ for (let i2 = 0;i2 < toUpload.length; i2++) {
223602
+ const output = toUpload[i2];
223537
223603
  const filePath = join6(cwd, output.outfile);
223538
- uploadSpinner.text = `Uploading ${output.name}... (${i2 + 1}/${outputs.length})`;
223604
+ uploadSpinner.text = `Uploading ${output.name}... (${i2 + 1}/${toUpload.length})`;
223539
223605
  const result = await uploadFile(filePath, uploadConfig);
223540
223606
  results.push(result);
223541
223607
  }
package/dist/config.d.ts CHANGED
@@ -1,28 +1,52 @@
1
1
  import { z } from "zod";
2
2
  export declare function expandPath(p: string): string;
3
+ /**
4
+ * Bundle configuration. Accepts a string pattern, array of patterns, or object.
5
+ * Patterns prefixed with `!` are exclusions. Patterns prefixed with `+` force
6
+ * inclusion (bypass .gitignore).
7
+ */
3
8
  declare const BundleConfigSchema: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodObject<{
4
9
  include: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
5
10
  outfile: z.ZodOptional<z.ZodString>;
6
11
  index: z.ZodDefault<z.ZodBoolean>;
7
12
  }, z.core.$strip>]>;
13
+ /**
14
+ * Upload destination configuration.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * upload: {
19
+ * provider: "gdrive",
20
+ * clientId: process.env.GDRIVE_CLIENT_ID,
21
+ * clientSecret: process.env.GDRIVE_CLIENT_SECRET,
22
+ * folderId: "1abc...",
23
+ * exclude: ["local", "debug"],
24
+ * }
25
+ * ```
26
+ */
8
27
  declare const UploadConfigSchema: z.ZodObject<{
9
28
  provider: z.ZodLiteral<"gdrive">;
10
29
  folderId: z.ZodOptional<z.ZodString>;
11
30
  clientId: z.ZodString;
12
31
  clientSecret: z.ZodString;
32
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
13
33
  }, z.core.$strip>;
34
+ /** Root configuration for srcpack. */
14
35
  declare const ConfigSchema: z.ZodObject<{
15
36
  outDir: z.ZodDefault<z.ZodString>;
37
+ emptyOutDir: z.ZodOptional<z.ZodBoolean>;
16
38
  upload: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
17
39
  provider: z.ZodLiteral<"gdrive">;
18
40
  folderId: z.ZodOptional<z.ZodString>;
19
41
  clientId: z.ZodString;
20
42
  clientSecret: z.ZodString;
43
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
44
  }, z.core.$strip>, z.ZodArray<z.ZodObject<{
22
45
  provider: z.ZodLiteral<"gdrive">;
23
46
  folderId: z.ZodOptional<z.ZodString>;
24
47
  clientId: z.ZodString;
25
48
  clientSecret: z.ZodString;
49
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
26
50
  }, z.core.$strip>>]>>;
27
51
  bundles: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodObject<{
28
52
  include: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
package/dist/index.js CHANGED
@@ -187020,10 +187020,12 @@ var UploadConfigSchema = exports_external.object({
187020
187020
  provider: exports_external.literal("gdrive"),
187021
187021
  folderId: exports_external.string().optional(),
187022
187022
  clientId: exports_external.string().min(1),
187023
- clientSecret: exports_external.string().min(1)
187023
+ clientSecret: exports_external.string().min(1),
187024
+ exclude: exports_external.array(exports_external.string()).optional()
187024
187025
  });
187025
187026
  var ConfigSchema = exports_external.object({
187026
187027
  outDir: exports_external.string().default(".srcpack"),
187028
+ emptyOutDir: exports_external.boolean().optional(),
187027
187029
  upload: exports_external.union([UploadConfigSchema, exports_external.array(UploadConfigSchema).min(1)]).optional(),
187028
187030
  bundles: exports_external.record(exports_external.string(), BundleConfigSchema)
187029
187031
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "srcpack",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Zero-config CLI for bundling code into LLM-optimized context files",
5
5
  "keywords": [
6
6
  "llm",
package/src/bundle.ts CHANGED
@@ -86,20 +86,79 @@ function isExcluded(filePath: string, matchers: Matcher[]): boolean {
86
86
  }
87
87
 
88
88
  /**
89
- * Load and parse .gitignore file from a directory
89
+ * Convert gitignore patterns to glob ignore patterns for fast-glob.
90
+ * This prevents traversing into ignored directories (performance optimization).
91
+ *
92
+ * Conservative approach: only convert simple, unambiguous directory patterns.
93
+ * Complex patterns (negations, root-anchored, globs) are left to the ignore filter.
90
94
  */
91
- async function loadGitignore(cwd: string): Promise<Ignore> {
95
+ function gitignoreToGlobPatterns(lines: string[]): string[] {
96
+ // If any negation patterns exist, skip optimization entirely
97
+ // (negations could re-include files in otherwise-ignored directories)
98
+ const hasNegation = lines.some((line) => {
99
+ const trimmed = line.trim();
100
+ // Any line starting with ! is a negation (including !#file which negates "#file")
101
+ return trimmed.startsWith("!");
102
+ });
103
+ if (hasNegation) return [];
104
+
105
+ const patterns: string[] = [];
106
+
107
+ for (const line of lines) {
108
+ const trimmed = line.trim();
109
+ // Skip empty lines and comments
110
+ if (!trimmed || trimmed.startsWith("#")) continue;
111
+
112
+ // Skip patterns with special gitignore features we can't safely convert:
113
+ // - Root-anchored (starts with /)
114
+ // - Contains globs (*, ?, [)
115
+ // - Contains path separators (complex paths)
116
+ // - Escaped characters
117
+ if (
118
+ trimmed.startsWith("/") ||
119
+ trimmed.includes("*") ||
120
+ trimmed.includes("?") ||
121
+ trimmed.includes("[") ||
122
+ trimmed.includes("/") ||
123
+ trimmed.includes("\\")
124
+ ) {
125
+ continue;
126
+ }
127
+
128
+ // Only convert simple directory names (e.g., "node_modules", "dist")
129
+ // These are safe to prune at any depth
130
+ const name = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
131
+ if (name && /^[\w.-]+$/.test(name)) {
132
+ patterns.push(`**/${name}/**`);
133
+ }
134
+ }
135
+
136
+ return patterns;
137
+ }
138
+
139
+ interface GitignoreResult {
140
+ ignore: Ignore;
141
+ globPatterns: string[];
142
+ }
143
+
144
+ /**
145
+ * Load and parse .gitignore file from a directory.
146
+ * Returns both an Ignore instance for filtering and glob patterns for fast-glob.
147
+ */
148
+ async function loadGitignore(cwd: string): Promise<GitignoreResult> {
92
149
  const ig = ignore();
93
150
  const gitignorePath = join(cwd, ".gitignore");
151
+ let globPatterns: string[] = [];
94
152
 
95
153
  try {
96
154
  const content = await readFile(gitignorePath, "utf-8");
97
155
  ig.add(content);
156
+ globPatterns = gitignoreToGlobPatterns(content.split("\n"));
98
157
  } catch {
99
158
  // No .gitignore file, return empty ignore instance
100
159
  }
101
160
 
102
- return ig;
161
+ return { ignore: ig, globPatterns };
103
162
  }
104
163
 
105
164
  /**
@@ -114,12 +173,18 @@ export async function resolvePatterns(
114
173
  ): Promise<string[]> {
115
174
  const { include, exclude, force } = normalizePatterns(config);
116
175
  const excludeMatchers = exclude.map((p) => picomatch(p));
117
- const gitignore = await loadGitignore(cwd);
176
+ const { ignore: gitignore, globPatterns } = await loadGitignore(cwd);
118
177
  const files = new Set<string>();
119
178
 
120
179
  // Regular includes: respect .gitignore
180
+ // Pass gitignore patterns to fast-glob to skip ignored directories during traversal
121
181
  if (include.length > 0) {
122
- const matches = await glob(include, { cwd, onlyFiles: true, dot: true });
182
+ const matches = await glob(include, {
183
+ cwd,
184
+ onlyFiles: true,
185
+ dot: true,
186
+ ignore: globPatterns,
187
+ });
123
188
  for (const match of matches) {
124
189
  if (!isExcluded(match, excludeMatchers) && !gitignore.ignores(match)) {
125
190
  const fullPath = join(cwd, match);
@@ -130,7 +195,7 @@ export async function resolvePatterns(
130
195
  }
131
196
  }
132
197
 
133
- // Force includes: bypass .gitignore
198
+ // Force includes: bypass .gitignore (no ignore patterns passed to glob)
134
199
  if (force.length > 0) {
135
200
  const matches = await glob(force, { cwd, onlyFiles: true, dot: true });
136
201
  for (const match of matches) {
package/src/cli.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- import { mkdir, writeFile } from "node:fs/promises";
5
- import { dirname, join } from "node:path";
4
+ import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
5
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
6
  import ora from "ora";
7
7
  import { bundleOne, type BundleResult } from "./bundle.ts";
8
8
  import {
@@ -38,6 +38,31 @@ function plural(n: number, singular: string, pluralForm?: string): string {
38
38
  return n === 1 ? singular : (pluralForm ?? singular + "s");
39
39
  }
40
40
 
41
+ function isOutDirInsideRoot(outDir: string, root: string): boolean {
42
+ const absoluteOutDir = isAbsolute(outDir) ? outDir : resolve(root, outDir);
43
+ const rel = relative(root, absoluteOutDir);
44
+ return !rel.startsWith("..") && !isAbsolute(rel);
45
+ }
46
+
47
+ /**
48
+ * Empty a directory while preserving specified entries (e.g., `.git`).
49
+ * Uses `force: true` to handle read-only or in-use files.
50
+ */
51
+ async function emptyDirectory(dir: string, skip: string[] = []): Promise<void> {
52
+ let entries: string[];
53
+ try {
54
+ entries = await readdir(dir);
55
+ } catch {
56
+ return; // Directory doesn't exist, nothing to empty
57
+ }
58
+ const skipSet = new Set(skip);
59
+ await Promise.all(
60
+ entries
61
+ .filter((entry) => !skipSet.has(entry))
62
+ .map((entry) => rm(join(dir, entry), { recursive: true, force: true })),
63
+ );
64
+ }
65
+
41
66
  async function main() {
42
67
  const args = process.argv.slice(2);
43
68
 
@@ -54,9 +79,11 @@ Usage:
54
79
  npx srcpack login Authenticate with Google Drive
55
80
 
56
81
  Options:
57
- --dry-run Preview bundles without writing files
58
- --no-upload Skip uploading to cloud storage
59
- -h, --help Show this help message
82
+ --dry-run Preview bundles without writing files
83
+ --emptyOutDir Empty output directory before bundling
84
+ --no-emptyOutDir Keep existing files in output directory
85
+ --no-upload Skip uploading to cloud storage
86
+ -h, --help Show this help message
60
87
  `);
61
88
  return;
62
89
  }
@@ -73,6 +100,12 @@ Options:
73
100
 
74
101
  const dryRun = args.includes("--dry-run");
75
102
  const noUpload = args.includes("--no-upload");
103
+ // CLI flags: --emptyOutDir forces true, --no-emptyOutDir forces false
104
+ const emptyOutDirFlag = args.includes("--emptyOutDir")
105
+ ? true
106
+ : args.includes("--no-emptyOutDir")
107
+ ? false
108
+ : undefined;
76
109
  const subcommands = ["init", "login"];
77
110
  const requestedBundles = args.filter(
78
111
  (arg) => !arg.startsWith("-") && !subcommands.includes(arg),
@@ -106,6 +139,31 @@ Options:
106
139
  }
107
140
 
108
141
  const cwd = process.cwd();
142
+
143
+ // Resolve emptyOutDir: CLI flag > config > auto (true if inside root)
144
+ const outDirInsideRoot = isOutDirInsideRoot(config.outDir, cwd);
145
+ const emptyOutDir = emptyOutDirFlag ?? config.emptyOutDir ?? outDirInsideRoot;
146
+
147
+ // Warn if outDir is outside root and emptyOutDir is not explicitly set
148
+ if (
149
+ !outDirInsideRoot &&
150
+ emptyOutDirFlag === undefined &&
151
+ config.emptyOutDir === undefined
152
+ ) {
153
+ console.warn(
154
+ `Warning: outDir "${config.outDir}" is outside project root. ` +
155
+ "Use --emptyOutDir to suppress this warning and empty the directory.",
156
+ );
157
+ }
158
+
159
+ // Empty outDir before bundling (unless dry-run)
160
+ if (emptyOutDir && !dryRun) {
161
+ const outDirPath = isAbsolute(config.outDir)
162
+ ? config.outDir
163
+ : resolve(cwd, config.outDir);
164
+ await emptyDirectory(outDirPath, [".git"]);
165
+ }
166
+
109
167
  const outputs: BundleOutput[] = [];
110
168
 
111
169
  // Process all bundles with progress
@@ -279,6 +337,15 @@ async function handleGdriveUpload(
279
337
  outputs: BundleOutput[],
280
338
  cwd: string,
281
339
  ): Promise<void> {
340
+ // Filter out excluded bundles
341
+ const excludeSet = new Set(uploadConfig.exclude ?? []);
342
+ const toUpload = outputs.filter((o) => !excludeSet.has(o.name));
343
+
344
+ if (toUpload.length === 0) {
345
+ console.log("\nNo bundles to upload (all excluded).");
346
+ return;
347
+ }
348
+
282
349
  try {
283
350
  await ensureAuthenticated(uploadConfig);
284
351
 
@@ -289,10 +356,10 @@ async function handleGdriveUpload(
289
356
 
290
357
  const results: UploadResult[] = [];
291
358
 
292
- for (let i = 0; i < outputs.length; i++) {
293
- const output = outputs[i]!;
359
+ for (let i = 0; i < toUpload.length; i++) {
360
+ const output = toUpload[i]!;
294
361
  const filePath = join(cwd, output.outfile);
295
- uploadSpinner.text = `Uploading ${output.name}... (${i + 1}/${outputs.length})`;
362
+ uploadSpinner.text = `Uploading ${output.name}... (${i + 1}/${toUpload.length})`;
296
363
  const result = await uploadFile(filePath, uploadConfig);
297
364
  results.push(result);
298
365
  }
package/src/config.ts CHANGED
@@ -12,33 +12,68 @@ export function expandPath(p: string): string {
12
12
  return p;
13
13
  }
14
14
 
15
+ /** Glob patterns for file matching. Single pattern or array of patterns. */
15
16
  const PatternsSchema = z.union([
16
17
  z.string().min(1),
17
18
  z.array(z.string().min(1)).min(1),
18
19
  ]);
19
20
 
21
+ /**
22
+ * Bundle configuration. Accepts a string pattern, array of patterns, or object.
23
+ * Patterns prefixed with `!` are exclusions. Patterns prefixed with `+` force
24
+ * inclusion (bypass .gitignore).
25
+ */
20
26
  const BundleConfigSchema = z.union([
21
- z.string().min(1), // "src/**/*"
22
- z.array(z.string().min(1)).min(1), // ["src/**/*", "!src/specs"]
27
+ z.string().min(1),
28
+ z.array(z.string().min(1)).min(1),
23
29
  z.object({
30
+ /** Glob patterns to include in the bundle. */
24
31
  include: PatternsSchema,
32
+ /** Custom output file path. Defaults to `<outDir>/<bundleName>.txt`. */
25
33
  outfile: z.string().optional(),
26
- index: z.boolean().default(true), // Include index header in output
34
+ /** Include file index header in output. Defaults to true. */
35
+ index: z.boolean().default(true),
27
36
  }),
28
37
  ]);
29
38
 
39
+ /**
40
+ * Upload destination configuration.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * upload: {
45
+ * provider: "gdrive",
46
+ * clientId: process.env.GDRIVE_CLIENT_ID,
47
+ * clientSecret: process.env.GDRIVE_CLIENT_SECRET,
48
+ * folderId: "1abc...",
49
+ * exclude: ["local", "debug"],
50
+ * }
51
+ * ```
52
+ */
30
53
  const UploadConfigSchema = z.object({
54
+ /** Upload provider. Currently only "gdrive" is supported. */
31
55
  provider: z.literal("gdrive"),
56
+ /** Google Drive folder ID to upload files to. If omitted, uploads to root. */
32
57
  folderId: z.string().optional(),
58
+ /** OAuth 2.0 client ID from Google Cloud Console. */
33
59
  clientId: z.string().min(1),
60
+ /** OAuth 2.0 client secret from Google Cloud Console. */
34
61
  clientSecret: z.string().min(1),
62
+ /** Bundle names to skip during upload. Supports exact names only. */
63
+ exclude: z.array(z.string()).optional(),
35
64
  });
36
65
 
66
+ /** Root configuration for srcpack. */
37
67
  const ConfigSchema = z.object({
68
+ /** Output directory for bundle files. Defaults to ".srcpack". */
38
69
  outDir: z.string().default(".srcpack"),
70
+ /** Empty outDir before bundling. Auto-enabled when outDir is inside project root. */
71
+ emptyOutDir: z.boolean().optional(),
72
+ /** Upload configuration for cloud storage. Single destination or array. */
39
73
  upload: z
40
74
  .union([UploadConfigSchema, z.array(UploadConfigSchema).min(1)])
41
75
  .optional(),
76
+ /** Named bundles mapping bundle name to glob patterns or config object. */
42
77
  bundles: z.record(z.string(), BundleConfigSchema),
43
78
  });
44
79