@p4pilot/core 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.
@@ -0,0 +1,453 @@
1
+ // src/types.ts
2
+ var P4PilotError = class extends Error {
3
+ constructor(message, code, detail) {
4
+ super(message);
5
+ this.code = code;
6
+ this.detail = detail;
7
+ this.name = "P4PilotError";
8
+ }
9
+ code;
10
+ detail;
11
+ };
12
+
13
+ // src/ztag.ts
14
+ function parseZtag(stdout) {
15
+ const records = [];
16
+ let record = /* @__PURE__ */ new Map();
17
+ let previousKey;
18
+ const finishRecord = () => {
19
+ if (record.size > 0) {
20
+ records.push(record);
21
+ record = /* @__PURE__ */ new Map();
22
+ }
23
+ previousKey = void 0;
24
+ };
25
+ for (const line of stdout.split(/\r?\n/)) {
26
+ if (line.length === 0) {
27
+ finishRecord();
28
+ } else if (line.startsWith("... ")) {
29
+ const field = line.slice(4);
30
+ const separatorIndex = field.indexOf(" ");
31
+ const key = separatorIndex === -1 ? field : field.slice(0, separatorIndex);
32
+ const value = separatorIndex === -1 ? "" : field.slice(separatorIndex + 1);
33
+ record.set(key, value);
34
+ previousKey = key;
35
+ } else if (previousKey !== void 0) {
36
+ const previousValue = record.get(previousKey) ?? "";
37
+ record.set(previousKey, `${previousValue}
38
+ ${line}`);
39
+ }
40
+ }
41
+ finishRecord();
42
+ return records;
43
+ }
44
+ function groupIndexed(record) {
45
+ const grouped = {};
46
+ const indexed = /* @__PURE__ */ new Map();
47
+ for (const [key, value] of record) {
48
+ const match = /^(.*?)(\d+)$/.exec(key);
49
+ const baseKey = match?.[1];
50
+ const indexText = match?.[2];
51
+ if (baseKey === void 0 || indexText === void 0) {
52
+ grouped[key] = value;
53
+ continue;
54
+ }
55
+ const values = indexed.get(baseKey) ?? [];
56
+ values.push({ index: Number(indexText), value });
57
+ indexed.set(baseKey, values);
58
+ }
59
+ for (const [key, values] of indexed) {
60
+ grouped[key] = values.sort((left, right) => left.index - right.index).map(({ value }) => value);
61
+ }
62
+ return grouped;
63
+ }
64
+
65
+ // src/p4-client.ts
66
+ var CHANGE_CREATED = /Change (\d+) created/;
67
+ function num(value) {
68
+ return value === void 0 ? void 0 : Number(value);
69
+ }
70
+ function str(value) {
71
+ return typeof value === "string" ? value : void 0;
72
+ }
73
+ function asArray(value) {
74
+ if (value === void 0) return [];
75
+ return Array.isArray(value) ? value : [value];
76
+ }
77
+ function toOpenedFile(record) {
78
+ return {
79
+ depotFile: record.get("depotFile") ?? "",
80
+ clientFile: record.get("clientFile"),
81
+ rev: num(record.get("rev")),
82
+ action: record.get("action") ?? "edit",
83
+ change: record.get("change") ?? "default",
84
+ type: record.get("type") ?? "text"
85
+ };
86
+ }
87
+ function toFileStat(record) {
88
+ const headRev = num(record.get("headRev"));
89
+ const action = record.get("action");
90
+ return {
91
+ depotFile: record.get("depotFile") ?? "",
92
+ clientFile: record.get("clientFile"),
93
+ headType: record.get("headType"),
94
+ headRev,
95
+ haveRev: num(record.get("haveRev")),
96
+ action,
97
+ isOpened: action !== void 0,
98
+ isTracked: headRev !== void 0
99
+ };
100
+ }
101
+ function toChangelistSummary(record) {
102
+ const status = record.get("status");
103
+ return {
104
+ change: record.get("change") ?? "",
105
+ description: record.get("desc") ?? "",
106
+ status: status === "submitted" || status === "shelved" ? status : "pending",
107
+ user: record.get("user"),
108
+ client: record.get("client")
109
+ };
110
+ }
111
+ var P4Client = class {
112
+ #runner;
113
+ constructor(runner) {
114
+ this.#runner = runner;
115
+ }
116
+ async #run(args, opts) {
117
+ const result = await this.#runner.run(args, opts);
118
+ if (result.exitCode !== 0) {
119
+ throw new P4PilotError(
120
+ `p4 ${args.join(" ")} failed`,
121
+ "P4_COMMAND_FAILED",
122
+ result.stderr.trim() || result.stdout.trim()
123
+ );
124
+ }
125
+ return result;
126
+ }
127
+ async info() {
128
+ const { stdout } = await this.#run(["info"]);
129
+ const record = parseZtag(stdout)[0];
130
+ const info = {};
131
+ if (record) {
132
+ for (const [key, value] of record) {
133
+ info[key] = value;
134
+ }
135
+ }
136
+ return info;
137
+ }
138
+ async opened(opts) {
139
+ const args = ["opened"];
140
+ if (opts?.changelist) args.push("-c", opts.changelist);
141
+ const { stdout } = await this.#run(args);
142
+ return parseZtag(stdout).map(toOpenedFile);
143
+ }
144
+ async fstat(files) {
145
+ if (files.length === 0) return [];
146
+ const { stdout } = await this.#run(["fstat", ...files]);
147
+ return parseZtag(stdout).map(toFileStat);
148
+ }
149
+ async edit(files, opts) {
150
+ return this.#openOp("edit", files, opts);
151
+ }
152
+ async add(files, opts) {
153
+ return this.#openOp("add", files, opts);
154
+ }
155
+ async deleteFiles(files, opts) {
156
+ return this.#openOp("delete", files, opts);
157
+ }
158
+ async #openOp(command, files, opts) {
159
+ if (files.length === 0) return [];
160
+ const args = [command];
161
+ if (opts?.changelist) args.push("-c", opts.changelist);
162
+ args.push(...files);
163
+ const { stdout } = await this.#run(args);
164
+ return parseZtag(stdout).map(toOpenedFile);
165
+ }
166
+ async revert(files) {
167
+ if (files.length === 0) return [];
168
+ const { stdout } = await this.#run(["revert", ...files]);
169
+ return parseZtag(stdout).map((record) => record.get("depotFile") ?? "").filter((depotFile) => depotFile.length > 0);
170
+ }
171
+ async sync(paths) {
172
+ const args = ["sync"];
173
+ if (paths && paths.length > 0) args.push(...paths);
174
+ const { stdout } = await this.#run(args);
175
+ return { synced: parseZtag(stdout).length };
176
+ }
177
+ async where(file) {
178
+ const { stdout } = await this.#run(["where", file]);
179
+ const record = parseZtag(stdout)[0];
180
+ if (!record) {
181
+ throw new P4PilotError(
182
+ `p4 where returned no mapping for ${file}`,
183
+ "FILE_NOT_IN_CLIENT"
184
+ );
185
+ }
186
+ const clientFile = record.get("clientFile") ?? "";
187
+ return {
188
+ depotFile: record.get("depotFile") ?? "",
189
+ clientFile,
190
+ path: record.get("path") ?? clientFile
191
+ };
192
+ }
193
+ async changes(opts) {
194
+ const args = ["changes"];
195
+ if (opts?.status) args.push("-s", opts.status);
196
+ if (opts?.max !== void 0) args.push("-m", String(opts.max));
197
+ if (opts?.user) args.push("-u", opts.user);
198
+ const { stdout } = await this.#run(args);
199
+ return parseZtag(stdout).map(toChangelistSummary);
200
+ }
201
+ async describe(change, opts) {
202
+ const args = ["describe"];
203
+ if (opts?.diff) args.push("-du");
204
+ args.push(change);
205
+ const { stdout } = await this.#run(args);
206
+ const record = parseZtag(stdout)[0];
207
+ if (!record) {
208
+ throw new P4PilotError(
209
+ `p4 describe returned nothing for ${change}`,
210
+ "P4_COMMAND_FAILED"
211
+ );
212
+ }
213
+ const grouped = groupIndexed(record);
214
+ const depotFiles = asArray(grouped.depotFile);
215
+ const actions = asArray(grouped.action);
216
+ const revs = asArray(grouped.rev);
217
+ const files = depotFiles.map((depotFile, index) => ({
218
+ depotFile,
219
+ action: actions[index] ?? "edit",
220
+ rev: revs[index] === void 0 ? void 0 : Number(revs[index])
221
+ }));
222
+ return {
223
+ change: str(grouped.change) ?? change,
224
+ description: str(grouped.desc) ?? "",
225
+ user: str(grouped.user),
226
+ files,
227
+ diff: str(grouped.diff)
228
+ };
229
+ }
230
+ async filelog(file, opts) {
231
+ const args = ["filelog"];
232
+ if (opts?.max !== void 0) args.push("-m", String(opts.max));
233
+ args.push(file);
234
+ const { stdout } = await this.#run(args);
235
+ const record = parseZtag(stdout)[0];
236
+ if (!record) return [];
237
+ const grouped = groupIndexed(record);
238
+ const revs = asArray(grouped.rev);
239
+ const changes = asArray(grouped.change);
240
+ const actions = asArray(grouped.action);
241
+ const users = asArray(grouped.user);
242
+ const descriptions = asArray(grouped.desc);
243
+ return revs.map((rev, index) => ({
244
+ rev: Number(rev),
245
+ change: changes[index] ?? "",
246
+ action: actions[index] ?? "edit",
247
+ user: users[index] ?? "",
248
+ description: descriptions[index] ?? ""
249
+ }));
250
+ }
251
+ async newChangelist(description) {
252
+ const indented = description.replace(/\n/g, "\n ");
253
+ const spec = `Change: new
254
+
255
+ Description:
256
+ ${indented}
257
+ `;
258
+ const { stdout } = await this.#run(["change", "-i"], { input: spec });
259
+ const matched = CHANGE_CREATED.exec(stdout);
260
+ if (matched?.[1]) return matched[1];
261
+ const change = parseZtag(stdout)[0]?.get("change");
262
+ if (change) return change;
263
+ throw new P4PilotError(
264
+ "could not parse created changelist number",
265
+ "P4_COMMAND_FAILED",
266
+ stdout.trim()
267
+ );
268
+ }
269
+ async reopen(files, changelist) {
270
+ if (files.length === 0) return [];
271
+ const { stdout } = await this.#run(["reopen", "-c", changelist, ...files]);
272
+ return parseZtag(stdout).map(toOpenedFile);
273
+ }
274
+ };
275
+
276
+ // src/asset-guard.ts
277
+ var DEFAULT_ASSET_GUARD_CONFIG = {
278
+ largeAssetExtensions: [
279
+ ".uasset",
280
+ ".umap",
281
+ ".fbx",
282
+ ".psd",
283
+ ".pak",
284
+ ".mp4",
285
+ ".mov",
286
+ ".blend",
287
+ ".exr"
288
+ ],
289
+ binaryExtensions: [
290
+ ".png",
291
+ ".tga",
292
+ ".jpg",
293
+ ".jpeg",
294
+ ".gif",
295
+ ".bmp",
296
+ ".dds",
297
+ ".ico",
298
+ ".wav",
299
+ ".mp3",
300
+ ".ogg",
301
+ ".flac",
302
+ ".bin",
303
+ ".dll",
304
+ ".exe",
305
+ ".so",
306
+ ".dylib",
307
+ ".lib",
308
+ ".a",
309
+ ".zip",
310
+ ".7z",
311
+ ".rar",
312
+ ".gz",
313
+ ".pdf"
314
+ ],
315
+ maxTextBytes: 1e6
316
+ };
317
+ function extname(path) {
318
+ const normalized = path.replace(/\\/g, "/");
319
+ const base = normalized.slice(normalized.lastIndexOf("/") + 1);
320
+ const dot = base.lastIndexOf(".");
321
+ return dot <= 0 ? "" : base.slice(dot).toLowerCase();
322
+ }
323
+ function isBinaryFiletype(filetype) {
324
+ if (filetype === void 0) return false;
325
+ return /(binary|ubinary|apple)/i.test(filetype);
326
+ }
327
+ function build(path, kind, filetype, sizeBytes, reason) {
328
+ return {
329
+ path,
330
+ kind,
331
+ filetype,
332
+ sizeBytes,
333
+ shouldRead: kind === "text",
334
+ reason
335
+ };
336
+ }
337
+ function classifyAsset(path, opts) {
338
+ const config = {
339
+ ...DEFAULT_ASSET_GUARD_CONFIG,
340
+ ...opts?.config
341
+ };
342
+ const ext = extname(path);
343
+ const filetype = opts?.stat?.headType;
344
+ const sizeBytes = opts?.sizeBytes;
345
+ if (config.largeAssetExtensions.includes(ext)) {
346
+ return build(
347
+ path,
348
+ "large-asset",
349
+ filetype,
350
+ sizeBytes,
351
+ `large-asset extension ${ext}`
352
+ );
353
+ }
354
+ if (isBinaryFiletype(filetype)) {
355
+ return build(
356
+ path,
357
+ "binary",
358
+ filetype,
359
+ sizeBytes,
360
+ `binary p4 filetype ${filetype ?? ""}`
361
+ );
362
+ }
363
+ if (config.binaryExtensions.includes(ext)) {
364
+ return build(
365
+ path,
366
+ "binary",
367
+ filetype,
368
+ sizeBytes,
369
+ `binary extension ${ext}`
370
+ );
371
+ }
372
+ if (sizeBytes !== void 0 && sizeBytes > config.maxTextBytes) {
373
+ return build(
374
+ path,
375
+ "binary",
376
+ filetype,
377
+ sizeBytes,
378
+ `size ${sizeBytes} exceeds maxTextBytes ${config.maxTextBytes}`
379
+ );
380
+ }
381
+ return build(
382
+ path,
383
+ "text",
384
+ filetype,
385
+ sizeBytes,
386
+ ext ? `text extension ${ext}` : "text file"
387
+ );
388
+ }
389
+
390
+ // src/auto-checkout.ts
391
+ async function ensureOpenForEdit(client, localPath, opts) {
392
+ const [stat] = await client.fstat([localPath]);
393
+ const asset = classifyAsset(localPath, { stat, config: opts?.assetConfig });
394
+ const changelist = opts?.changelist;
395
+ const openOpts = changelist === void 0 ? void 0 : { changelist };
396
+ if (stat?.isOpened) {
397
+ return {
398
+ path: localPath,
399
+ status: "already-open",
400
+ action: stat.action,
401
+ changelist,
402
+ asset
403
+ };
404
+ }
405
+ if (stat?.isTracked) {
406
+ const [opened] = await client.edit([localPath], openOpts);
407
+ return {
408
+ path: localPath,
409
+ status: "opened",
410
+ action: opened?.action ?? "edit",
411
+ changelist: opened?.change ?? changelist,
412
+ asset
413
+ };
414
+ }
415
+ const [added] = await client.add([localPath], openOpts);
416
+ return {
417
+ path: localPath,
418
+ status: "added",
419
+ action: added?.action ?? "add",
420
+ changelist: added?.change ?? changelist,
421
+ asset
422
+ };
423
+ }
424
+ async function ensureOpenForEditMany(client, localPaths, opts) {
425
+ const results = [];
426
+ for (const path of localPaths) {
427
+ try {
428
+ results.push(await ensureOpenForEdit(client, path, opts));
429
+ } catch {
430
+ results.push({ path, status: "skipped-untracked-ignored" });
431
+ }
432
+ }
433
+ return results;
434
+ }
435
+
436
+ // src/changelist.ts
437
+ function buildChangelistDescription(intent, prefix = "[p4pilot] ") {
438
+ const trimmed = intent.trim();
439
+ if (trimmed.length === 0) return prefix.trimEnd();
440
+ return trimmed.startsWith(prefix) ? trimmed : `${prefix}${trimmed}`;
441
+ }
442
+
443
+ export {
444
+ P4PilotError,
445
+ parseZtag,
446
+ groupIndexed,
447
+ P4Client,
448
+ DEFAULT_ASSET_GUARD_CONFIG,
449
+ classifyAsset,
450
+ ensureOpenForEdit,
451
+ ensureOpenForEditMany,
452
+ buildChangelistDescription
453
+ };