@the-coded/pstash 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 +703 -0
- package/dist/bin/pstash.d.ts +2 -0
- package/dist/bin/pstash.js +2924 -0
- package/dist/bin/pstash.js.map +1 -0
- package/dist/index.d.ts +286 -0
- package/dist/index.js +368 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module schemas
|
|
5
|
+
*
|
|
6
|
+
* Zod as Single Source of Truth (SSoT) for all types.
|
|
7
|
+
*
|
|
8
|
+
* **Rule**: No manual `interface` or `type` declarations anywhere in the codebase.
|
|
9
|
+
* All types are derived via `z.infer<typeof Schema>`.
|
|
10
|
+
*
|
|
11
|
+
* Validation happens at every I/O boundary:
|
|
12
|
+
* - Reading `.stash.json` and `.project.json` files
|
|
13
|
+
* - Reading `~/.pstashrc` config
|
|
14
|
+
* - Parsing CLI options
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Parse and validate untrusted data
|
|
18
|
+
* const metadata = StashMetadataSchema.parse(JSON.parse(rawJson))
|
|
19
|
+
*
|
|
20
|
+
* // Get TypeScript type from schema
|
|
21
|
+
* type Metadata = z.infer<typeof StashMetadataSchema>
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Represents a single file entry within a stash.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const file: StashFile = {
|
|
29
|
+
* name: "README.md",
|
|
30
|
+
* size: 1024,
|
|
31
|
+
* hash: "sha256:a1b2c3d4e5f6"
|
|
32
|
+
* }
|
|
33
|
+
*/
|
|
34
|
+
declare const StashFileSchema: z.ZodObject<{
|
|
35
|
+
name: z.ZodString;
|
|
36
|
+
size: z.ZodNumber;
|
|
37
|
+
hash: z.ZodString;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
type StashFile = z.infer<typeof StashFileSchema>;
|
|
40
|
+
/**
|
|
41
|
+
* Metadata stored in `.stash.json` inside each stash directory.
|
|
42
|
+
* Created by `Stasher.save()`, read by `Stasher.loadMetadata()`.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // Stash directory structure:
|
|
46
|
+
* // my-personal-stash/scena/2026-03-12_01-05_k7x2/.stash.json
|
|
47
|
+
*/
|
|
48
|
+
declare const StashMetadataSchema: z.ZodObject<{
|
|
49
|
+
id: z.ZodString;
|
|
50
|
+
project: z.ZodString;
|
|
51
|
+
timestamp: z.ZodString;
|
|
52
|
+
updatedAt: z.ZodOptional<z.ZodString>;
|
|
53
|
+
message: z.ZodString;
|
|
54
|
+
tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
55
|
+
branch: z.ZodOptional<z.ZodString>;
|
|
56
|
+
commit: z.ZodOptional<z.ZodString>;
|
|
57
|
+
user: z.ZodOptional<z.ZodString>;
|
|
58
|
+
files: z.ZodArray<z.ZodObject<{
|
|
59
|
+
name: z.ZodString;
|
|
60
|
+
size: z.ZodNumber;
|
|
61
|
+
hash: z.ZodString;
|
|
62
|
+
}, z.core.$strip>>;
|
|
63
|
+
totalSize: z.ZodNumber;
|
|
64
|
+
compressed: z.ZodDefault<z.ZodBoolean>;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
type StashMetadata = z.infer<typeof StashMetadataSchema>;
|
|
67
|
+
/**
|
|
68
|
+
* Metadata stored in `.project.json` at the project directory root.
|
|
69
|
+
* Managed by `Indexer` — updated after every save/delete.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* // my-personal-stash/scena/.project.json
|
|
73
|
+
* {
|
|
74
|
+
* "name": "scena",
|
|
75
|
+
* "stashCount": 3,
|
|
76
|
+
* "totalSize": "268 KB"
|
|
77
|
+
* }
|
|
78
|
+
*/
|
|
79
|
+
declare const ProjectMetadataSchema: z.ZodObject<{
|
|
80
|
+
name: z.ZodString;
|
|
81
|
+
remote: z.ZodOptional<z.ZodString>;
|
|
82
|
+
aliases: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
83
|
+
stashCount: z.ZodNumber;
|
|
84
|
+
totalSize: z.ZodString;
|
|
85
|
+
createdAt: z.ZodString;
|
|
86
|
+
updatedAt: z.ZodString;
|
|
87
|
+
}, z.core.$strip>;
|
|
88
|
+
type ProjectMetadata = z.infer<typeof ProjectMetadataSchema>;
|
|
89
|
+
/**
|
|
90
|
+
* Per-project configuration inside `~/.pstashrc`.
|
|
91
|
+
* All fields are optional — only needed to customize behavior.
|
|
92
|
+
*/
|
|
93
|
+
declare const ProjectConfigSchema: z.ZodObject<{
|
|
94
|
+
aliases: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
95
|
+
remote: z.ZodOptional<z.ZodString>;
|
|
96
|
+
path: z.ZodOptional<z.ZodString>;
|
|
97
|
+
}, z.core.$strip>;
|
|
98
|
+
type ProjectConfig = z.infer<typeof ProjectConfigSchema>;
|
|
99
|
+
/**
|
|
100
|
+
* Global configuration stored at `~/.pstashrc`.
|
|
101
|
+
*
|
|
102
|
+
* - Location: `os.homedir() + "/.pstashrc"` (cross-platform)
|
|
103
|
+
* - Never stored inside the data repo (`my-personal-stash`)
|
|
104
|
+
* - Loaded and validated by `src/config/loader.ts`
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* {
|
|
108
|
+
* "version": "1.0.0",
|
|
109
|
+
* "remote": "git@github.com:user/my-personal-stash.git",
|
|
110
|
+
* "localPath": "~/.pstash",
|
|
111
|
+
* "autoSync": true,
|
|
112
|
+
* "projects": {},
|
|
113
|
+
* "defaults": { "removeAfterSave": false }
|
|
114
|
+
* }
|
|
115
|
+
*/
|
|
116
|
+
declare const GlobalConfigSchema: z.ZodObject<{
|
|
117
|
+
version: z.ZodString;
|
|
118
|
+
remote: z.ZodString;
|
|
119
|
+
localPath: z.ZodDefault<z.ZodString>;
|
|
120
|
+
autoSync: z.ZodDefault<z.ZodBoolean>;
|
|
121
|
+
projects: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
122
|
+
aliases: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
123
|
+
remote: z.ZodOptional<z.ZodString>;
|
|
124
|
+
path: z.ZodOptional<z.ZodString>;
|
|
125
|
+
}, z.core.$strip>>>;
|
|
126
|
+
defaults: z.ZodObject<{
|
|
127
|
+
keepOnPop: z.ZodDefault<z.ZodBoolean>;
|
|
128
|
+
compression: z.ZodDefault<z.ZodBoolean>;
|
|
129
|
+
removeAfterSave: z.ZodDefault<z.ZodBoolean>;
|
|
130
|
+
}, z.core.$strip>;
|
|
131
|
+
}, z.core.$strip>;
|
|
132
|
+
type GlobalConfig = z.infer<typeof GlobalConfigSchema>;
|
|
133
|
+
/**
|
|
134
|
+
* Options for the `pstash save` command.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* pstash save -t docs -t wip "planning notes" *.md
|
|
138
|
+
*/
|
|
139
|
+
declare const SaveOptionsSchema: z.ZodObject<{
|
|
140
|
+
message: z.ZodString;
|
|
141
|
+
files: z.ZodArray<z.ZodString>;
|
|
142
|
+
tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
143
|
+
project: z.ZodOptional<z.ZodString>;
|
|
144
|
+
push: z.ZodDefault<z.ZodBoolean>;
|
|
145
|
+
compress: z.ZodDefault<z.ZodBoolean>;
|
|
146
|
+
removeAfterSave: z.ZodOptional<z.ZodBoolean>;
|
|
147
|
+
}, z.core.$strip>;
|
|
148
|
+
type SaveOptions = z.infer<typeof SaveOptionsSchema>;
|
|
149
|
+
/**
|
|
150
|
+
* Options for the `pstash list` command.
|
|
151
|
+
*/
|
|
152
|
+
declare const ListOptionsSchema: z.ZodObject<{
|
|
153
|
+
all: z.ZodDefault<z.ZodBoolean>;
|
|
154
|
+
project: z.ZodOptional<z.ZodString>;
|
|
155
|
+
tag: z.ZodOptional<z.ZodString>;
|
|
156
|
+
since: z.ZodOptional<z.ZodString>;
|
|
157
|
+
until: z.ZodOptional<z.ZodString>;
|
|
158
|
+
preview: z.ZodDefault<z.ZodBoolean>;
|
|
159
|
+
json: z.ZodDefault<z.ZodBoolean>;
|
|
160
|
+
}, z.core.$strip>;
|
|
161
|
+
type ListOptions = z.infer<typeof ListOptionsSchema>;
|
|
162
|
+
/**
|
|
163
|
+
* Options for `pstash pop` and `pstash apply` commands.
|
|
164
|
+
*/
|
|
165
|
+
declare const RestoreOptionsSchema: z.ZodObject<{
|
|
166
|
+
stashIndex: z.ZodOptional<z.ZodNumber>;
|
|
167
|
+
files: z.ZodOptional<z.ZodString>;
|
|
168
|
+
dest: z.ZodOptional<z.ZodString>;
|
|
169
|
+
keep: z.ZodDefault<z.ZodBoolean>;
|
|
170
|
+
force: z.ZodDefault<z.ZodBoolean>;
|
|
171
|
+
}, z.core.$strip>;
|
|
172
|
+
type RestoreOptions = z.infer<typeof RestoreOptionsSchema>;
|
|
173
|
+
/**
|
|
174
|
+
* Options for `pstash init` command.
|
|
175
|
+
*/
|
|
176
|
+
declare const InitOptionsSchema: z.ZodObject<{
|
|
177
|
+
remote: z.ZodOptional<z.ZodString>;
|
|
178
|
+
path: z.ZodOptional<z.ZodString>;
|
|
179
|
+
}, z.core.$strip>;
|
|
180
|
+
type InitOptions = z.infer<typeof InitOptionsSchema>;
|
|
181
|
+
/**
|
|
182
|
+
* Options for `pstash sync` command.
|
|
183
|
+
*/
|
|
184
|
+
declare const SyncOptionsSchema: z.ZodObject<{
|
|
185
|
+
pull: z.ZodDefault<z.ZodBoolean>;
|
|
186
|
+
push: z.ZodDefault<z.ZodBoolean>;
|
|
187
|
+
}, z.core.$strip>;
|
|
188
|
+
type SyncOptions = z.infer<typeof SyncOptionsSchema>;
|
|
189
|
+
/**
|
|
190
|
+
* Options for `pstash status` command.
|
|
191
|
+
*/
|
|
192
|
+
declare const StatusOptionsSchema: z.ZodObject<{
|
|
193
|
+
all: z.ZodDefault<z.ZodBoolean>;
|
|
194
|
+
json: z.ZodDefault<z.ZodBoolean>;
|
|
195
|
+
}, z.core.$strip>;
|
|
196
|
+
type StatusOptions = z.infer<typeof StatusOptionsSchema>;
|
|
197
|
+
/**
|
|
198
|
+
* Options for `pstash drop` command.
|
|
199
|
+
*/
|
|
200
|
+
declare const DropOptionsSchema: z.ZodObject<{
|
|
201
|
+
stashIndex: z.ZodOptional<z.ZodNumber>;
|
|
202
|
+
project: z.ZodOptional<z.ZodString>;
|
|
203
|
+
tag: z.ZodOptional<z.ZodString>;
|
|
204
|
+
all: z.ZodDefault<z.ZodBoolean>;
|
|
205
|
+
force: z.ZodDefault<z.ZodBoolean>;
|
|
206
|
+
}, z.core.$strip>;
|
|
207
|
+
type DropOptions = z.infer<typeof DropOptionsSchema>;
|
|
208
|
+
/**
|
|
209
|
+
* Options for `pstash clean` command (Phase 3).
|
|
210
|
+
*/
|
|
211
|
+
declare const CleanOptionsSchema: z.ZodObject<{
|
|
212
|
+
olderThan: z.ZodOptional<z.ZodString>;
|
|
213
|
+
keep: z.ZodOptional<z.ZodNumber>;
|
|
214
|
+
tag: z.ZodOptional<z.ZodString>;
|
|
215
|
+
dryRun: z.ZodDefault<z.ZodBoolean>;
|
|
216
|
+
}, z.core.$strip>;
|
|
217
|
+
type CleanOptions = z.infer<typeof CleanOptionsSchema>;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @module config/loader
|
|
221
|
+
*
|
|
222
|
+
* Load, save, and validate the global pstash config (`~/.pstashrc`).
|
|
223
|
+
*
|
|
224
|
+
* The config file is a JSON document validated against
|
|
225
|
+
* {@link GlobalConfigSchema}. All paths support `~/` notation, which
|
|
226
|
+
* is expanded against `os.homedir()` for cross-platform compatibility.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* import { loadConfig, updateConfig } from "pstash/config/loader"
|
|
231
|
+
*
|
|
232
|
+
* const config = await loadConfig()
|
|
233
|
+
* await updateConfig({ autoSync: false })
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
/** Absolute path to the global config file */
|
|
238
|
+
declare const CONFIG_PATH: string;
|
|
239
|
+
/**
|
|
240
|
+
* Resolves the stash repo local path from config.
|
|
241
|
+
* Handles "~/.pstash" notation → expands to absolute path.
|
|
242
|
+
*/
|
|
243
|
+
declare function resolveLocalPath(localPath: string): string;
|
|
244
|
+
/**
|
|
245
|
+
* Checks whether the config file exists.
|
|
246
|
+
*/
|
|
247
|
+
declare function configExists(): Promise<boolean>;
|
|
248
|
+
/**
|
|
249
|
+
* Loads and validates ~/.pstashrc.
|
|
250
|
+
* Throws if file not found or invalid.
|
|
251
|
+
*/
|
|
252
|
+
declare function loadConfig(): Promise<GlobalConfig>;
|
|
253
|
+
/**
|
|
254
|
+
* Saves config to ~/.pstashrc.
|
|
255
|
+
*/
|
|
256
|
+
declare function saveConfig(config: GlobalConfig): Promise<void>;
|
|
257
|
+
/**
|
|
258
|
+
* Updates a specific key in the config.
|
|
259
|
+
*/
|
|
260
|
+
declare function updateConfig(updates: Partial<GlobalConfig>): Promise<GlobalConfig>;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @module config/templates
|
|
264
|
+
*
|
|
265
|
+
* Default config templates and seed files for fresh pstash installs.
|
|
266
|
+
*
|
|
267
|
+
* Provides:
|
|
268
|
+
* - {@link createDefaultConfig} — builds a {@link GlobalConfig} from defaults
|
|
269
|
+
* - {@link DATA_REPO_README} — initial README for the data repo
|
|
270
|
+
* - {@link DATA_REPO_GITIGNORE} — initial `.gitignore` for the data repo
|
|
271
|
+
*/
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Creates a default GlobalConfig for new pstash installations.
|
|
275
|
+
*/
|
|
276
|
+
declare function createDefaultConfig(remote: string, localPath?: string): GlobalConfig;
|
|
277
|
+
/**
|
|
278
|
+
* README template for the data repo (my-personal-stash).
|
|
279
|
+
*/
|
|
280
|
+
declare const DATA_REPO_README = "# My Personal Stash\n\n> Managed by [pstash](https://github.com/the-coded/pstash-cli) \u2014 Git-backed personal file stash.\n\n## Structure\n\n```\nmy-personal-stash/\n\u251C\u2500\u2500 <project-name>/\n\u2502 \u251C\u2500\u2500 .project.json # Project metadata\n\u2502 \u2514\u2500\u2500 YYYY-MM-DD_HH-mm_XXXX/ # Stash entries\n\u2502 \u251C\u2500\u2500 .stash.json # Stash metadata\n\u2502 \u2514\u2500\u2500 <files...>\n```\n\n## Usage\n\nThis repo is managed automatically by `pstash`. Do not edit manually.\n\n```bash\npstash save \"message\" *.md # Save files\npstash list # List stashes\npstash pop 0 # Restore latest\npstash sync # Sync with remote\n```\n";
|
|
281
|
+
/**
|
|
282
|
+
* .gitignore template for the data repo.
|
|
283
|
+
*/
|
|
284
|
+
declare const DATA_REPO_GITIGNORE = ".DS_Store\nThumbs.db\n*.log\n.env\n.env.*\n!.env.example\n";
|
|
285
|
+
|
|
286
|
+
export { CONFIG_PATH, type CleanOptions, CleanOptionsSchema, DATA_REPO_GITIGNORE, DATA_REPO_README, type DropOptions, DropOptionsSchema, type GlobalConfig, GlobalConfigSchema, type InitOptions, InitOptionsSchema, type ListOptions, ListOptionsSchema, type ProjectConfig, ProjectConfigSchema, type ProjectMetadata, ProjectMetadataSchema, type RestoreOptions, RestoreOptionsSchema, type SaveOptions, SaveOptionsSchema, type StashFile, StashFileSchema, type StashMetadata, StashMetadataSchema, type StatusOptions, StatusOptionsSchema, type SyncOptions, SyncOptionsSchema, configExists, createDefaultConfig, loadConfig, resolveLocalPath, saveConfig, updateConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/schemas.ts
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var StashFileSchema = z.object({
|
|
6
|
+
/** Original filename (basename only, no path) */
|
|
7
|
+
name: z.string(),
|
|
8
|
+
/** File size in bytes */
|
|
9
|
+
size: z.number().nonnegative(),
|
|
10
|
+
/**
|
|
11
|
+
* SHA-256 integrity hash, prefixed with "sha256:".
|
|
12
|
+
* First 12 hex chars used (48 bits) — sufficient for integrity checking.
|
|
13
|
+
* @example "sha256:a1b2c3d4e5f6"
|
|
14
|
+
*/
|
|
15
|
+
hash: z.string()
|
|
16
|
+
});
|
|
17
|
+
var StashMetadataSchema = z.object({
|
|
18
|
+
/**
|
|
19
|
+
* Unique stash identifier.
|
|
20
|
+
* Format: `YYYY-MM-DD_HH-mm_XXXX` (timestamp + 4-char nanoid suffix).
|
|
21
|
+
* The suffix prevents collisions when multiple machines stash in the same minute.
|
|
22
|
+
* @example "2026-03-12_01-05_k7x2"
|
|
23
|
+
*/
|
|
24
|
+
id: z.string(),
|
|
25
|
+
/** Project name (derived from git remote or directory name) */
|
|
26
|
+
project: z.string(),
|
|
27
|
+
/** ISO 8601 creation timestamp */
|
|
28
|
+
timestamp: z.string().datetime(),
|
|
29
|
+
/**
|
|
30
|
+
* ISO 8601 timestamp of the last `pstash update` on this stash.
|
|
31
|
+
* Absent when the stash has never been updated.
|
|
32
|
+
*/
|
|
33
|
+
updatedAt: z.string().datetime().optional(),
|
|
34
|
+
/** Human-readable description of what was stashed */
|
|
35
|
+
message: z.string(),
|
|
36
|
+
/**
|
|
37
|
+
* User-defined tags for filtering and organization.
|
|
38
|
+
* @example ["docs", "planning", "wip"]
|
|
39
|
+
*/
|
|
40
|
+
tags: z.array(z.string()).default([]),
|
|
41
|
+
/** Git branch at time of stash (if inside a git repo) */
|
|
42
|
+
branch: z.string().optional(),
|
|
43
|
+
/** Git commit hash at time of stash (if inside a git repo) */
|
|
44
|
+
commit: z.string().optional(),
|
|
45
|
+
/**
|
|
46
|
+
* Machine identifier of who created the stash.
|
|
47
|
+
* Format: `username@hostname` using `os.userInfo().username` (cross-platform).
|
|
48
|
+
* @example "gab@macmini"
|
|
49
|
+
*/
|
|
50
|
+
user: z.string().optional(),
|
|
51
|
+
/** List of stashed files with size and integrity hash */
|
|
52
|
+
files: z.array(StashFileSchema),
|
|
53
|
+
/** Total size of all files in bytes */
|
|
54
|
+
totalSize: z.number().nonnegative(),
|
|
55
|
+
/** Whether files are stored as tar.gz (Phase 3 feature) */
|
|
56
|
+
compressed: z.boolean().default(false)
|
|
57
|
+
});
|
|
58
|
+
var ProjectMetadataSchema = z.object({
|
|
59
|
+
/** Project name (matches directory name in stash repo) */
|
|
60
|
+
name: z.string(),
|
|
61
|
+
/** Git remote URL for this project (informational only) */
|
|
62
|
+
remote: z.string().optional(),
|
|
63
|
+
/**
|
|
64
|
+
* Alternative names that map to this project.
|
|
65
|
+
* @example ["scena-cli", "e2e-gen"] → resolves to "scena"
|
|
66
|
+
*/
|
|
67
|
+
aliases: z.array(z.string()).default([]),
|
|
68
|
+
/** Total number of stash entries for this project */
|
|
69
|
+
stashCount: z.number().nonnegative(),
|
|
70
|
+
/**
|
|
71
|
+
* Human-readable total size of all stashes.
|
|
72
|
+
* Formatted via `pretty-bytes`.
|
|
73
|
+
* @example "268 KB"
|
|
74
|
+
*/
|
|
75
|
+
totalSize: z.string(),
|
|
76
|
+
/** ISO 8601 timestamp of first stash creation */
|
|
77
|
+
createdAt: z.string().datetime(),
|
|
78
|
+
/** ISO 8601 timestamp of last stash modification */
|
|
79
|
+
updatedAt: z.string().datetime()
|
|
80
|
+
});
|
|
81
|
+
var ProjectConfigSchema = z.object({
|
|
82
|
+
/**
|
|
83
|
+
* Alternative names that resolve to this project.
|
|
84
|
+
* @example ["e2e-gen", "scena-cli"] both map to "scena"
|
|
85
|
+
*/
|
|
86
|
+
aliases: z.array(z.string()).default([]),
|
|
87
|
+
/**
|
|
88
|
+
* Override the detected git remote for this project.
|
|
89
|
+
* Useful if the project name differs from the remote.
|
|
90
|
+
*/
|
|
91
|
+
remote: z.string().optional(),
|
|
92
|
+
/** Absolute path to the project directory */
|
|
93
|
+
path: z.string().optional()
|
|
94
|
+
});
|
|
95
|
+
var GlobalConfigSchema = z.object({
|
|
96
|
+
/** Config schema version for future migrations */
|
|
97
|
+
version: z.string(),
|
|
98
|
+
/** SSH or HTTPS URL of the personal stash data repository (SSH or HTTPS) */
|
|
99
|
+
remote: z.string().min(1, "Remote URL is required"),
|
|
100
|
+
/**
|
|
101
|
+
* Local path for the cloned stash data repo.
|
|
102
|
+
* Supports `~` expansion: `"~/.pstash"` → `"/Users/gab/.pstash"`.
|
|
103
|
+
* Resolved by `resolveLocalPath()` in `config/loader.ts`.
|
|
104
|
+
*/
|
|
105
|
+
localPath: z.string().default("~/.pstash"),
|
|
106
|
+
/**
|
|
107
|
+
* Whether to automatically pull before and push after write operations
|
|
108
|
+
* (save, pop, drop, clean) and pull before read operations (list).
|
|
109
|
+
* Can be overridden per-operation with `--no-sync`.
|
|
110
|
+
*/
|
|
111
|
+
autoSync: z.boolean().default(true),
|
|
112
|
+
/**
|
|
113
|
+
* Per-project overrides (aliases, custom remote, path).
|
|
114
|
+
* Key is the canonical project name.
|
|
115
|
+
*/
|
|
116
|
+
projects: z.record(z.string(), ProjectConfigSchema).default({}),
|
|
117
|
+
/** Default behavior settings (can be overridden per-command) */
|
|
118
|
+
defaults: z.object({
|
|
119
|
+
/** If true, `pstash pop` keeps the stash after restoring (like apply) */
|
|
120
|
+
keepOnPop: z.boolean().default(false),
|
|
121
|
+
/** If true, compress stash files as tar.gz (Phase 3) */
|
|
122
|
+
compression: z.boolean().default(false),
|
|
123
|
+
/**
|
|
124
|
+
* If true, delete source files after `pstash save`.
|
|
125
|
+
* Can be overridden per-operation: `--rm` to force delete, `--keep` to force keep.
|
|
126
|
+
*/
|
|
127
|
+
removeAfterSave: z.boolean().default(false)
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
var SaveOptionsSchema = z.object({
|
|
131
|
+
/** Required description of what was stashed */
|
|
132
|
+
message: z.string().min(1, "Message is required"),
|
|
133
|
+
/**
|
|
134
|
+
* File patterns to stash (glob patterns supported via globby).
|
|
135
|
+
* @example ["*.md", "src/**\/*.ts"]
|
|
136
|
+
*/
|
|
137
|
+
files: z.array(z.string()).min(1, "At least one file pattern required"),
|
|
138
|
+
/** Tags for filtering and organization */
|
|
139
|
+
tags: z.array(z.string()).default([]),
|
|
140
|
+
/** Override auto-detected project name */
|
|
141
|
+
project: z.string().optional(),
|
|
142
|
+
/** Whether to push to remote after saving */
|
|
143
|
+
push: z.boolean().default(true),
|
|
144
|
+
/** Whether to compress the stash (Phase 3) */
|
|
145
|
+
compress: z.boolean().default(false),
|
|
146
|
+
/**
|
|
147
|
+
* Whether to remove source files after saving.
|
|
148
|
+
* `undefined` = use `config.defaults.removeAfterSave`.
|
|
149
|
+
* `true` (--rm) = always remove.
|
|
150
|
+
* `false` (--keep) = always keep.
|
|
151
|
+
*/
|
|
152
|
+
removeAfterSave: z.boolean().optional()
|
|
153
|
+
});
|
|
154
|
+
var ListOptionsSchema = z.object({
|
|
155
|
+
/** Show stashes from all projects (not just current) */
|
|
156
|
+
all: z.boolean().default(false),
|
|
157
|
+
/** Filter by specific project name */
|
|
158
|
+
project: z.string().optional(),
|
|
159
|
+
/** Filter by tag */
|
|
160
|
+
tag: z.string().optional(),
|
|
161
|
+
/**
|
|
162
|
+
* Show stashes created after this timespec.
|
|
163
|
+
* @example "7d" (7 days ago), "2w", "1m", "2026-03-01"
|
|
164
|
+
*/
|
|
165
|
+
since: z.string().optional(),
|
|
166
|
+
/** Show stashes created before this timespec */
|
|
167
|
+
until: z.string().optional(),
|
|
168
|
+
/** Show preview of the first non-empty line of up to the first 3 files per stash */
|
|
169
|
+
preview: z.boolean().default(false),
|
|
170
|
+
/** Output as JSON for scripting */
|
|
171
|
+
json: z.boolean().default(false)
|
|
172
|
+
});
|
|
173
|
+
var RestoreOptionsSchema = z.object({
|
|
174
|
+
/**
|
|
175
|
+
* Index of the stash to restore (0-based, newest first).
|
|
176
|
+
* If undefined, shows interactive selector.
|
|
177
|
+
*/
|
|
178
|
+
stashIndex: z.number().int().nonnegative().optional(),
|
|
179
|
+
/**
|
|
180
|
+
* Glob pattern for partial restore (Phase 3, requires micromatch).
|
|
181
|
+
* @example "*.md" to restore only markdown files
|
|
182
|
+
*/
|
|
183
|
+
files: z.string().optional(),
|
|
184
|
+
/**
|
|
185
|
+
* Destination directory for restored files.
|
|
186
|
+
* Defaults to `process.cwd()`.
|
|
187
|
+
*/
|
|
188
|
+
dest: z.string().optional(),
|
|
189
|
+
/** If true, keep the stash after restoring (apply behavior) */
|
|
190
|
+
keep: z.boolean().default(false),
|
|
191
|
+
/** If true, overwrite existing files without error */
|
|
192
|
+
force: z.boolean().default(false)
|
|
193
|
+
});
|
|
194
|
+
var InitOptionsSchema = z.object({
|
|
195
|
+
/** SSH or HTTPS URL for the data repo (SSH or HTTPS) */
|
|
196
|
+
remote: z.string().min(1).optional(),
|
|
197
|
+
/** Local path to clone to (default: ~/.pstash) */
|
|
198
|
+
path: z.string().optional()
|
|
199
|
+
});
|
|
200
|
+
var SyncOptionsSchema = z.object({
|
|
201
|
+
/** Pull only (skip push) */
|
|
202
|
+
pull: z.boolean().default(false),
|
|
203
|
+
/** Push only (skip pull) */
|
|
204
|
+
push: z.boolean().default(false)
|
|
205
|
+
});
|
|
206
|
+
var StatusOptionsSchema = z.object({
|
|
207
|
+
/** Show status for all projects */
|
|
208
|
+
all: z.boolean().default(false),
|
|
209
|
+
/** Output as JSON for scripting */
|
|
210
|
+
json: z.boolean().default(false)
|
|
211
|
+
});
|
|
212
|
+
var DropOptionsSchema = z.object({
|
|
213
|
+
/** Index of the stash to drop (0-based). If undefined, shows interactive selector. */
|
|
214
|
+
stashIndex: z.number().int().nonnegative().optional(),
|
|
215
|
+
/** Limit to a specific project */
|
|
216
|
+
project: z.string().optional(),
|
|
217
|
+
/** Drop all stashes with this tag */
|
|
218
|
+
tag: z.string().optional(),
|
|
219
|
+
/** Drop all stashes in the project (requires confirmation) */
|
|
220
|
+
all: z.boolean().default(false),
|
|
221
|
+
/** Skip confirmation prompt */
|
|
222
|
+
force: z.boolean().default(false)
|
|
223
|
+
});
|
|
224
|
+
var CleanOptionsSchema = z.object({
|
|
225
|
+
/**
|
|
226
|
+
* Delete stashes older than this timespec.
|
|
227
|
+
* @example "30d", "2w", "1m"
|
|
228
|
+
*/
|
|
229
|
+
olderThan: z.string().optional(),
|
|
230
|
+
/** Keep only N most recent stashes per project */
|
|
231
|
+
keep: z.number().int().positive().optional(),
|
|
232
|
+
/** Delete only stashes with this tag */
|
|
233
|
+
tag: z.string().optional(),
|
|
234
|
+
/** Preview what would be deleted without actually deleting */
|
|
235
|
+
dryRun: z.boolean().default(false)
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// src/config/loader.ts
|
|
239
|
+
import { readFile, writeFile, access } from "fs/promises";
|
|
240
|
+
import { join, resolve } from "path";
|
|
241
|
+
import { homedir } from "os";
|
|
242
|
+
var CONFIG_PATH = join(homedir(), ".pstashrc");
|
|
243
|
+
function resolveLocalPath(localPath) {
|
|
244
|
+
if (localPath.startsWith("~/")) {
|
|
245
|
+
return join(homedir(), localPath.slice(2));
|
|
246
|
+
}
|
|
247
|
+
if (localPath.startsWith("~")) {
|
|
248
|
+
return join(homedir(), localPath.slice(1));
|
|
249
|
+
}
|
|
250
|
+
return resolve(localPath);
|
|
251
|
+
}
|
|
252
|
+
async function configExists() {
|
|
253
|
+
try {
|
|
254
|
+
await access(CONFIG_PATH);
|
|
255
|
+
return true;
|
|
256
|
+
} catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function loadConfig() {
|
|
261
|
+
let raw;
|
|
262
|
+
try {
|
|
263
|
+
raw = await readFile(CONFIG_PATH, "utf-8");
|
|
264
|
+
} catch {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Config file not found: ${CONFIG_PATH}
|
|
267
|
+
Run "pstash init" to set up your personal stash.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
let parsed;
|
|
271
|
+
try {
|
|
272
|
+
parsed = JSON.parse(raw);
|
|
273
|
+
} catch {
|
|
274
|
+
throw new Error(`Config file is not valid JSON: ${CONFIG_PATH}`);
|
|
275
|
+
}
|
|
276
|
+
const result = GlobalConfigSchema.safeParse(parsed);
|
|
277
|
+
if (!result.success) {
|
|
278
|
+
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
279
|
+
throw new Error(`Config validation failed:
|
|
280
|
+
${issues}`);
|
|
281
|
+
}
|
|
282
|
+
return result.data;
|
|
283
|
+
}
|
|
284
|
+
async function saveConfig(config) {
|
|
285
|
+
const validated = GlobalConfigSchema.parse(config);
|
|
286
|
+
await writeFile(CONFIG_PATH, JSON.stringify(validated, null, 2) + "\n", "utf-8");
|
|
287
|
+
}
|
|
288
|
+
async function updateConfig(updates) {
|
|
289
|
+
const current = await loadConfig();
|
|
290
|
+
const merged = { ...current, ...updates };
|
|
291
|
+
const validated = GlobalConfigSchema.parse(merged);
|
|
292
|
+
await saveConfig(validated);
|
|
293
|
+
return validated;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/config/templates.ts
|
|
297
|
+
function createDefaultConfig(remote, localPath = "~/.pstash") {
|
|
298
|
+
return {
|
|
299
|
+
version: "1.0.0",
|
|
300
|
+
remote,
|
|
301
|
+
localPath,
|
|
302
|
+
autoSync: true,
|
|
303
|
+
projects: {},
|
|
304
|
+
defaults: {
|
|
305
|
+
keepOnPop: false,
|
|
306
|
+
compression: false,
|
|
307
|
+
removeAfterSave: false
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
var DATA_REPO_README = `# My Personal Stash
|
|
312
|
+
|
|
313
|
+
> Managed by [pstash](https://github.com/the-coded/pstash-cli) \u2014 Git-backed personal file stash.
|
|
314
|
+
|
|
315
|
+
## Structure
|
|
316
|
+
|
|
317
|
+
\`\`\`
|
|
318
|
+
my-personal-stash/
|
|
319
|
+
\u251C\u2500\u2500 <project-name>/
|
|
320
|
+
\u2502 \u251C\u2500\u2500 .project.json # Project metadata
|
|
321
|
+
\u2502 \u2514\u2500\u2500 YYYY-MM-DD_HH-mm_XXXX/ # Stash entries
|
|
322
|
+
\u2502 \u251C\u2500\u2500 .stash.json # Stash metadata
|
|
323
|
+
\u2502 \u2514\u2500\u2500 <files...>
|
|
324
|
+
\`\`\`
|
|
325
|
+
|
|
326
|
+
## Usage
|
|
327
|
+
|
|
328
|
+
This repo is managed automatically by \`pstash\`. Do not edit manually.
|
|
329
|
+
|
|
330
|
+
\`\`\`bash
|
|
331
|
+
pstash save "message" *.md # Save files
|
|
332
|
+
pstash list # List stashes
|
|
333
|
+
pstash pop 0 # Restore latest
|
|
334
|
+
pstash sync # Sync with remote
|
|
335
|
+
\`\`\`
|
|
336
|
+
`;
|
|
337
|
+
var DATA_REPO_GITIGNORE = `.DS_Store
|
|
338
|
+
Thumbs.db
|
|
339
|
+
*.log
|
|
340
|
+
.env
|
|
341
|
+
.env.*
|
|
342
|
+
!.env.example
|
|
343
|
+
`;
|
|
344
|
+
export {
|
|
345
|
+
CONFIG_PATH,
|
|
346
|
+
CleanOptionsSchema,
|
|
347
|
+
DATA_REPO_GITIGNORE,
|
|
348
|
+
DATA_REPO_README,
|
|
349
|
+
DropOptionsSchema,
|
|
350
|
+
GlobalConfigSchema,
|
|
351
|
+
InitOptionsSchema,
|
|
352
|
+
ListOptionsSchema,
|
|
353
|
+
ProjectConfigSchema,
|
|
354
|
+
ProjectMetadataSchema,
|
|
355
|
+
RestoreOptionsSchema,
|
|
356
|
+
SaveOptionsSchema,
|
|
357
|
+
StashFileSchema,
|
|
358
|
+
StashMetadataSchema,
|
|
359
|
+
StatusOptionsSchema,
|
|
360
|
+
SyncOptionsSchema,
|
|
361
|
+
configExists,
|
|
362
|
+
createDefaultConfig,
|
|
363
|
+
loadConfig,
|
|
364
|
+
resolveLocalPath,
|
|
365
|
+
saveConfig,
|
|
366
|
+
updateConfig
|
|
367
|
+
};
|
|
368
|
+
//# sourceMappingURL=index.js.map
|