isomorphic-jj 1.1.3 → 1.3.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/index.mjs CHANGED
@@ -1309,6 +1309,7 @@ class BookmarkStore {
1309
1309
  this.storage = storage;
1310
1310
  this.local = new Map();
1311
1311
  this.remote = new Map(); // remote → (bookmark → changeId)
1312
+ this.tracking = {}; // bookmark → { remote, ref } tracking info (v0.35.0)
1312
1313
  }
1313
1314
 
1314
1315
  /**
@@ -1351,6 +1352,7 @@ class BookmarkStore {
1351
1352
 
1352
1353
  this.local.clear();
1353
1354
  this.remote.clear();
1355
+ this.tracking = data.tracked || {}; // Load tracking info (v0.35.0)
1354
1356
 
1355
1357
  // Load local bookmarks
1356
1358
  for (const [name, target] of Object.entries(data.local)) {
@@ -1388,7 +1390,7 @@ class BookmarkStore {
1388
1390
  version: 1,
1389
1391
  local,
1390
1392
  remote,
1391
- tracked: {},
1393
+ tracked: this.tracking || {}, // Save tracking info (v0.35.0)
1392
1394
  });
1393
1395
  }
1394
1396
 
@@ -1501,13 +1503,23 @@ class BookmarkStore {
1501
1503
  async list() {
1502
1504
  const result = [];
1503
1505
 
1504
- // Add local bookmarks
1506
+ // Add local bookmarks with tracking info (v0.35.0)
1505
1507
  for (const [name, target] of this.local.entries()) {
1506
- result.push({
1508
+ const bookmark = {
1507
1509
  name,
1508
1510
  changeId: target,
1509
1511
  remote: null,
1510
- });
1512
+ };
1513
+
1514
+ // Include tracking info if available
1515
+ if (this.tracking && this.tracking[name]) {
1516
+ bookmark.tracking = {
1517
+ remote: this.tracking[name].remote,
1518
+ ref: this.tracking[name].remoteName || name,
1519
+ };
1520
+ }
1521
+
1522
+ result.push(bookmark);
1511
1523
  }
1512
1524
 
1513
1525
  // Add remote bookmarks
@@ -1829,6 +1841,24 @@ class RevsetEngine {
1829
1841
  }
1830
1842
  }
1831
1843
 
1844
+ // v0.36.0: visible() - non-abandoned changes
1845
+ if (trimmed === 'visible()') {
1846
+ await this.graph.load();
1847
+ const all = this.graph.getAll();
1848
+ return all
1849
+ .filter(c => !c.abandoned)
1850
+ .map(c => c.changeId);
1851
+ }
1852
+
1853
+ // v0.36.0: hidden() - abandoned changes
1854
+ if (trimmed === 'hidden()') {
1855
+ await this.graph.load();
1856
+ const all = this.graph.getAll();
1857
+ return all
1858
+ .filter(c => c.abandoned === true)
1859
+ .map(c => c.changeId);
1860
+ }
1861
+
1832
1862
  // ancestors(changeId) - all ancestors including the change itself
1833
1863
  const ancestorsMatch = trimmed.match(/^ancestors\(([0-9a-f]{32})\)$/);
1834
1864
  if (ancestorsMatch) {
@@ -4276,9 +4306,15 @@ class UserConfig {
4276
4306
  }
4277
4307
 
4278
4308
  /**
4279
- * Load config from storage
4309
+ * Load config from storage and/or programmatic object
4310
+ * v0.35.0: Also loads workspace-specific config from workspace-config.json
4311
+ * v0.36.0: Accepts optional config object for programmatic configuration
4312
+ *
4313
+ * @param {Object} [opts] - Optional config options
4314
+ * @param {Object} [opts.workspace] - Workspace-specific config to merge (highest priority)
4315
+ * @param {Object} [opts.override] - Config object to merge over loaded config
4280
4316
  */
4281
- async load() {
4317
+ async load(opts = {}) {
4282
4318
  try {
4283
4319
  const data = await this.storage.read('config.json');
4284
4320
  if (data) {
@@ -4288,6 +4324,45 @@ class UserConfig {
4288
4324
  // No config file yet, use defaults
4289
4325
  await this.init();
4290
4326
  }
4327
+
4328
+ // Merge programmatic override config if provided
4329
+ if (opts.override) {
4330
+ this.config = this._deepMerge(this.config, opts.override);
4331
+ }
4332
+
4333
+ // Load workspace-specific config from file (v0.35.0)
4334
+ try {
4335
+ const workspaceData = await this.storage.read('workspace-config.json');
4336
+ if (workspaceData) {
4337
+ // Merge workspace config over global config (workspace takes precedence)
4338
+ this.config = this._deepMerge(this.config, workspaceData);
4339
+ }
4340
+ } catch (error) {
4341
+ // No workspace config, that's fine
4342
+ }
4343
+
4344
+ // Merge programmatic workspace config if provided (highest priority)
4345
+ if (opts.workspace) {
4346
+ this.config = this._deepMerge(this.config, opts.workspace);
4347
+ }
4348
+ }
4349
+
4350
+ /**
4351
+ * Deep merge two config objects (workspace overrides global)
4352
+ * @private
4353
+ */
4354
+ _deepMerge(target, source) {
4355
+ const result = { ...target };
4356
+
4357
+ for (const key in source) {
4358
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
4359
+ result[key] = this._deepMerge(target[key] || {}, source[key]);
4360
+ } else {
4361
+ result[key] = source[key];
4362
+ }
4363
+ }
4364
+
4365
+ return result;
4291
4366
  }
4292
4367
 
4293
4368
  /**
@@ -6216,69 +6291,116 @@ async function createJJ(options) {
6216
6291
  * @param {string[]} [args.paths] - Paths to move between changes (for history operations)
6217
6292
  * @returns {Promise<Object>} Returns change object for history operations, file info for file operations
6218
6293
  */
6219
- async move(args) {
6220
- if (!args || typeof args !== 'object') {
6221
- throw new JJError('INVALID_ARGUMENT', 'Missing or invalid arguments', {
6222
- suggestion: 'Use file.move({ from, to }) for files or rebase({ changeId, newParent }) for history',
6294
+ /**
6295
+ * Move a file in the working copy
6296
+ *
6297
+ * @param {Object} args - Move file arguments
6298
+ * @param {string} args.from - Source file path
6299
+ * @param {string} args.to - Destination file path
6300
+ * @returns {Promise<Object>} Operation result including from, to paths and file stats
6301
+ */
6302
+ async moveFile(args) {
6303
+ if (!args.from || typeof args.from !== 'string') {
6304
+ throw new JJError('INVALID_ARGUMENT', 'Missing or invalid from path', {
6305
+ suggestion: 'Provide a valid source file path',
6306
+ provided: { from: args.from, type: typeof args.from },
6223
6307
  });
6224
6308
  }
6225
6309
 
6226
- // Detect operation type based on arguments
6227
- const isHistoryOp = this._detectHistoryOperation(args);
6310
+ if (!args.to || typeof args.to !== 'string') {
6311
+ throw new JJError('INVALID_ARGUMENT', 'Missing or invalid to path', {
6312
+ suggestion: 'Provide a valid destination file path',
6313
+ provided: { to: args.to, type: typeof args.to },
6314
+ });
6315
+ }
6228
6316
 
6229
- if (isHistoryOp) {
6230
- return await this._moveChange(args);
6231
- } else {
6232
- return await this._moveFile(args);
6317
+ // Prevent moving to the same location
6318
+ if (args.from === args.to) {
6319
+ throw new JJError('INVALID_OPERATION', 'Source and destination paths are the same', {
6320
+ path: args.from,
6321
+ suggestion: 'Specify a different destination path',
6322
+ });
6233
6323
  }
6234
- },
6235
6324
 
6236
- /**
6237
- * Detect if move operation is for history (rebase) or files
6238
- *
6239
- * @private
6240
- * @param {Object} args - Move arguments
6241
- * @returns {boolean} True if history operation, false if file operation
6242
- */
6243
- _detectHistoryOperation(args) {
6244
- // Explicit history operation indicators
6245
- if (args.paths || args.changeId || args.newParent) {
6246
- return true;
6325
+ // Prevent absolute paths
6326
+ if (args.from.startsWith('/') || args.to.startsWith('/')) {
6327
+ throw new JJError('INVALID_PATH', 'Absolute paths are not allowed', {
6328
+ suggestion: 'Use paths relative to the repository root',
6329
+ provided: { from: args.from, to: args.to },
6330
+ });
6247
6331
  }
6248
6332
 
6249
- // If only from/to are present, check if they look like change IDs
6250
- if (args.from && args.to && !args.changeId && !args.newParent && !args.paths) {
6251
- // Change IDs are 32-character hex strings
6252
- const changeIdPattern = /^[0-9a-f]{32}$/;
6253
- const fromLooksLikeChangeId = changeIdPattern.test(args.from);
6254
- const toLooksLikeChangeId = changeIdPattern.test(args.to);
6333
+ // Prevent parent directory traversal
6334
+ if (args.from.includes('..') || args.to.includes('..')) {
6335
+ throw new JJError('INVALID_PATH', 'Parent directory traversal (..) is not allowed', {
6336
+ suggestion: 'Use paths within the repository',
6337
+ provided: { from: args.from, to: args.to },
6338
+ });
6339
+ }
6255
6340
 
6256
- // If both look like change IDs, it's likely a history operation
6257
- // However, we err on the side of file operations to be safe
6258
- // Users should use explicit { changeId, newParent } for clarity
6259
- if (fromLooksLikeChangeId && toLooksLikeChangeId) {
6260
- // This is ambiguous - throw a helpful error
6261
- throw new JJError('AMBIGUOUS_OPERATION',
6262
- 'Ambiguous move operation: arguments look like change IDs', {
6263
- suggestion: 'Use { changeId, newParent } for history operations or ensure file paths don\'t match change ID pattern',
6264
- args,
6265
- });
6266
- }
6341
+ const fromPath = `${dir}/${args.from}`;
6342
+ const toPath = `${dir}/${args.to}`;
6267
6343
 
6268
- return false; // Treat as file operation
6344
+ // Check if source file exists
6345
+ try {
6346
+ await fs.promises.stat(fromPath);
6347
+ } catch (error) {
6348
+ throw new JJError('FILE_NOT_FOUND', `Source file not found: ${args.from}`, {
6349
+ path: args.from,
6350
+ suggestion: 'Check that the source file exists',
6351
+ });
6269
6352
  }
6270
6353
 
6271
- return false;
6354
+ // Ensure destination directory exists
6355
+ const toDir = toPath.substring(0, toPath.lastIndexOf('/'));
6356
+ try {
6357
+ await fs.promises.mkdir(toDir, { recursive: true });
6358
+ } catch (error) {
6359
+ throw new JJError('FILE_SYSTEM_ERROR', `Failed to create destination directory: ${error.message}`, {
6360
+ path: toDir,
6361
+ error: error.message,
6362
+ });
6363
+ }
6364
+
6365
+ // Move the file
6366
+ try {
6367
+ await fs.promises.rename(fromPath, toPath);
6368
+ } catch (error) {
6369
+ throw new JJError('FILE_SYSTEM_ERROR', `Failed to move file: ${error.message}`, {
6370
+ from: args.from,
6371
+ to: args.to,
6372
+ error: error.message,
6373
+ });
6374
+ }
6375
+
6376
+ // Update working copy tracking
6377
+ await workingCopy.load();
6378
+ await workingCopy.untrackFile(args.from);
6379
+ const stats = await fs.promises.stat(toPath);
6380
+ await workingCopy.trackFile(args.to, {
6381
+ mtime: stats.mtime,
6382
+ size: stats.size,
6383
+ mode: stats.mode,
6384
+ });
6385
+
6386
+ return {
6387
+ from: args.from,
6388
+ to: args.to,
6389
+ size: stats.size,
6390
+ mode: stats.mode,
6391
+ mtime: stats.mtime,
6392
+ };
6272
6393
  },
6273
6394
 
6274
6395
  /**
6275
- * Move/rebase a change to a new parent (history operation)
6396
+ * Move (rebase) a change to a new parent
6276
6397
  *
6277
- * @private
6278
- * @param {Object} args - Move arguments
6398
+ * @param {Object} args - Move change arguments
6399
+ * @param {string} args.changeId - Change ID to move (also accepts 'from' or 'source')
6400
+ * @param {string} args.newParent - New parent change ID (also accepts 'to' or 'destination')
6279
6401
  * @returns {Promise<Object>} Updated change object
6280
6402
  */
6281
- async _moveChange(args) {
6403
+ async moveChange(args) {
6282
6404
  await graph.load();
6283
6405
 
6284
6406
  // Support multiple parameter names for flexibility
@@ -6366,110 +6488,62 @@ async function createJJ(options) {
6366
6488
  },
6367
6489
 
6368
6490
  /**
6369
- * Move/rename a file in the working copy (file operation)
6370
- *
6371
- * @private
6372
- * @param {Object} args - Move arguments
6373
- * @returns {Promise<Object>} Operation result including from, to paths and file stats
6491
+ * @deprecated Use moveFile() for files or moveChange() for history operations
6492
+ * Move operation - delegates to moveFile or moveChange based on arguments
6374
6493
  */
6375
- async _moveFile(args) {
6376
- if (!args.from || typeof args.from !== 'string') {
6377
- throw new JJError('INVALID_ARGUMENT', 'Missing or invalid from path', {
6378
- suggestion: 'Provide a valid source file path',
6379
- provided: { from: args.from, type: typeof args.from },
6380
- });
6381
- }
6382
-
6383
- if (!args.to || typeof args.to !== 'string') {
6384
- throw new JJError('INVALID_ARGUMENT', 'Missing or invalid to path', {
6385
- suggestion: 'Provide a valid destination file path',
6386
- provided: { to: args.to, type: typeof args.to },
6494
+ async move(args) {
6495
+ if (!args || typeof args !== 'object') {
6496
+ throw new JJError('INVALID_ARGUMENT', 'Missing or invalid arguments', {
6497
+ suggestion: 'Use moveFile({ from, to }) for files or moveChange({ changeId, newParent }) for history',
6387
6498
  });
6388
6499
  }
6389
6500
 
6390
- // Prevent moving to the same location
6391
- if (args.from === args.to) {
6392
- throw new JJError('INVALID_OPERATION', 'Source and destination paths are the same', {
6393
- path: args.from,
6394
- suggestion: 'Specify a different destination path',
6395
- });
6396
- }
6501
+ // Detect operation type based on arguments
6502
+ const isHistoryOp = this._detectHistoryOperation(args);
6397
6503
 
6398
- // Prevent absolute paths
6399
- if (args.from.startsWith('/') || args.to.startsWith('/')) {
6400
- throw new JJError('INVALID_PATH', 'Absolute paths are not allowed', {
6401
- suggestion: 'Use paths relative to the repository root',
6402
- provided: { from: args.from, to: args.to },
6403
- });
6504
+ if (isHistoryOp) {
6505
+ return await this.moveChange(args);
6506
+ } else {
6507
+ return await this.moveFile(args);
6404
6508
  }
6509
+ },
6405
6510
 
6406
- // Prevent parent directory traversal
6407
- if (args.from.includes('..') || args.to.includes('..')) {
6408
- throw new JJError('INVALID_PATH', 'Parent directory traversal (..) is not allowed', {
6409
- suggestion: 'Use paths within the repository',
6410
- provided: { from: args.from, to: args.to },
6411
- });
6511
+ /**
6512
+ * Detect if move operation is for history (rebase) or files
6513
+ *
6514
+ * @private
6515
+ * @param {Object} args - Move arguments
6516
+ * @returns {boolean} True if history operation, false if file operation
6517
+ */
6518
+ _detectHistoryOperation(args) {
6519
+ // Explicit history operation indicators
6520
+ if (args.paths || args.changeId || args.newParent) {
6521
+ return true;
6412
6522
  }
6413
6523
 
6414
- const fromPath = `${dir}/${args.from}`;
6415
- const toPath = `${dir}/${args.to}`;
6416
-
6417
- // Check if source file exists
6418
- try {
6419
- await fs.promises.access(fromPath);
6420
- } catch (error) {
6421
- throw new JJError('FILE_NOT_FOUND', `Source file not found: ${args.from}`, {
6422
- path: args.from,
6423
- fullPath: fromPath,
6424
- suggestion: 'Check that the file exists',
6425
- });
6426
- }
6524
+ // If only from/to are present, check if they look like change IDs
6525
+ if (args.from && args.to && !args.changeId && !args.newParent && !args.paths) {
6526
+ // Change IDs are 32-character hex strings
6527
+ const changeIdPattern = /^[0-9a-f]{32}$/;
6528
+ const fromLooksLikeChangeId = changeIdPattern.test(args.from);
6529
+ const toLooksLikeChangeId = changeIdPattern.test(args.to);
6427
6530
 
6428
- // Ensure destination directory exists
6429
- const pathParts = args.to.split('/');
6430
- if (pathParts.length > 1) {
6431
- const dirPath = pathParts.slice(0, -1).join('/');
6432
- const fullDirPath = `${dir}/${dirPath}`;
6433
- try {
6434
- await fs.promises.mkdir(fullDirPath, { recursive: true });
6435
- } catch (error) {
6436
- throw new JJError('DIRECTORY_CREATE_FAILED',
6437
- `Failed to create destination directory: ${dirPath}`, {
6438
- directory: dirPath,
6439
- originalError: error.message,
6531
+ // If both look like change IDs, it's likely a history operation
6532
+ // However, we err on the side of file operations to be safe
6533
+ // Users should use explicit { changeId, newParent } for clarity
6534
+ if (fromLooksLikeChangeId && toLooksLikeChangeId) {
6535
+ // This is ambiguous - throw a helpful error
6536
+ throw new JJError('AMBIGUOUS_OPERATION',
6537
+ 'Ambiguous move operation: arguments look like change IDs', {
6538
+ suggestion: 'Use { changeId, newParent } for history operations or ensure file paths don\'t match change ID pattern',
6539
+ args,
6440
6540
  });
6441
6541
  }
6442
- }
6443
6542
 
6444
- // Move the file
6445
- try {
6446
- await fs.promises.rename(fromPath, toPath);
6447
- } catch (error) {
6448
- throw new JJError('FILE_MOVE_FAILED', `Failed to move file: ${error.message}`, {
6449
- from: args.from,
6450
- to: args.to,
6451
- originalError: error.message,
6452
- });
6543
+ return false; // Treat as file operation
6453
6544
  }
6454
6545
 
6455
- // Update working copy tracking
6456
- await workingCopy.load();
6457
- await workingCopy.untrackFile(args.from);
6458
- const stats = await fs.promises.stat(toPath);
6459
- await workingCopy.trackFile(args.to, {
6460
- mtime: stats.mtime,
6461
- size: stats.size,
6462
- mode: stats.mode,
6463
- });
6464
-
6465
- // Return useful information about the move operation
6466
- return {
6467
- from: args.from,
6468
- to: args.to,
6469
- size: stats.size,
6470
- mode: stats.mode,
6471
- mtime: stats.mtime,
6472
- };
6546
+ return false;
6473
6547
  },
6474
6548
 
6475
6549
  /**
@@ -7014,7 +7088,8 @@ async function createJJ(options) {
7014
7088
  * @param {Object} args - Arguments
7015
7089
  * @param {string} [args.revset='all()'] - Revset expression to filter changes
7016
7090
  * @param {number} [args.limit] - Maximum number of changes to return
7017
- * @returns {Promise<Array>} Array of changes
7091
+ * @param {boolean} [args.count=false] - If true, return count instead of changes (v0.36.0)
7092
+ * @returns {Promise<Array|number>} Array of changes or count if args.count is true
7018
7093
  */
7019
7094
  async log(args = {}) {
7020
7095
  await graph.load();
@@ -7023,6 +7098,11 @@ async function createJJ(options) {
7023
7098
  const revsetExpr = args.revset || 'all()';
7024
7099
  const changeIds = await revset.evaluate(revsetExpr);
7025
7100
 
7101
+ // If count mode, return count early (v0.36.0 feature)
7102
+ if (args.count) {
7103
+ return changeIds.length;
7104
+ }
7105
+
7026
7106
  let changes = [];
7027
7107
  for (const changeId of changeIds) {
7028
7108
  const change = await graph.getChange(changeId);
@@ -7970,7 +8050,7 @@ async function createJJ(options) {
7970
8050
  * });
7971
8051
  */
7972
8052
  async rebase(args) {
7973
- return await this._moveChange(args);
8053
+ return await this.moveChange(args);
7974
8054
  },
7975
8055
 
7976
8056
  /**
@@ -8043,9 +8123,18 @@ async function createJJ(options) {
8043
8123
  * @returns {Promise<Object>} The new backout change including changeId, description, and backedOut field
8044
8124
  */
8045
8125
  async backout(args) {
8126
+ // Normalize: accept common parameter aliases
8127
+ if (args && args.change && !args.revision) {
8128
+ args = { ...args, revision: args.change };
8129
+ } else if (args && args.changeId && !args.revision) {
8130
+ args = { ...args, revision: args.changeId };
8131
+ } else if (args && args.target && !args.revision) {
8132
+ args = { ...args, revision: args.target };
8133
+ }
8134
+
8046
8135
  if (!args || !args.revision) {
8047
8136
  throw new JJError('INVALID_ARGUMENT', 'Missing revision argument', {
8048
- suggestion: 'Provide the change ID to back out: { revision: "abc123..." }',
8137
+ suggestion: 'Provide the change ID to back out: { revision: "abc123..." } (or use change/changeId/target)',
8049
8138
  });
8050
8139
  }
8051
8140
 
@@ -9080,6 +9169,15 @@ async function createJJ(options) {
9080
9169
  * @param {string[]} [args.paths] - Specific paths to include in first split (full implementation planned for future)
9081
9170
  */
9082
9171
  async split(args) {
9172
+ // Normalize: accept common parameter aliases
9173
+ if (args && args.change && !args.changeId) {
9174
+ args = { ...args, changeId: args.change };
9175
+ } else if (args && args.revision && !args.changeId) {
9176
+ args = { ...args, changeId: args.revision };
9177
+ } else if (args && args.target && !args.changeId) {
9178
+ args = { ...args, changeId: args.target };
9179
+ }
9180
+
9083
9181
  // Check for interactive mode (not supported)
9084
9182
  if (args.interactive) {
9085
9183
  throw new JJError(
@@ -11321,21 +11419,13 @@ async function createJJ(options) {
11321
11419
  });
11322
11420
  }
11323
11421
 
11324
- await userConfig.load();
11325
-
11326
- // Handle nested keys (e.g., 'user.name')
11327
- const parts = key.split('.');
11328
- let value = userConfig.config;
11329
-
11330
- for (const part of parts) {
11331
- if (value && typeof value === 'object') {
11332
- value = value[part];
11333
- } else {
11334
- return null;
11335
- }
11422
+ // Ensure config is loaded at least once
11423
+ if (!userConfig.config) {
11424
+ await userConfig.load();
11336
11425
  }
11337
11426
 
11338
- return value;
11427
+ // Use userConfig.get() to preserve programmatic config
11428
+ return userConfig.get(key);
11339
11429
  },
11340
11430
 
11341
11431
  /**
@@ -11384,6 +11474,87 @@ async function createJJ(options) {
11384
11474
  await userConfig.load();
11385
11475
  return userConfig.config;
11386
11476
  },
11477
+
11478
+ /**
11479
+ * Reload configuration (including workspace-specific config)
11480
+ * v0.35.0: Loads workspace-config.json if present
11481
+ * v0.36.0: Accepts optional config object for programmatic configuration
11482
+ *
11483
+ * @param {Object} [opts] - Optional config options
11484
+ * @param {Object} [opts.workspace] - Workspace-specific config to merge (highest priority)
11485
+ * @param {Object} [opts.override] - Config object to merge over loaded config
11486
+ * @returns {Promise<void>}
11487
+ *
11488
+ * @example
11489
+ * // Load from files only
11490
+ * await repo.config.load();
11491
+ *
11492
+ * @example
11493
+ * // Programmatic workspace config (no file I/O needed)
11494
+ * await repo.config.load({
11495
+ * workspace: { user: { email: 'test@example.com' } }
11496
+ * });
11497
+ *
11498
+ * @example
11499
+ * // Override global config programmatically
11500
+ * await repo.config.load({
11501
+ * override: { test: { value: 'override' } }
11502
+ * });
11503
+ */
11504
+ async load(opts) {
11505
+ await userConfig.load(opts);
11506
+ },
11507
+ },
11508
+
11509
+ /**
11510
+ * Template helper functions (v0.36.0)
11511
+ *
11512
+ * Utilities for displaying and formatting JJ data
11513
+ */
11514
+ template: {
11515
+ /**
11516
+ * Get files modified in a change
11517
+ *
11518
+ * @param {string} changeId - Change ID
11519
+ * @returns {Promise<string[]>} Array of file paths
11520
+ */
11521
+ async files(changeId) {
11522
+ await graph.load();
11523
+ const change = await graph.getChange(changeId);
11524
+ if (!change || !change.fileSnapshot) {
11525
+ return [];
11526
+ }
11527
+ return Object.keys(change.fileSnapshot);
11528
+ },
11529
+
11530
+ /**
11531
+ * Join array elements with a separator
11532
+ *
11533
+ * @param {string[]} array - Array of strings
11534
+ * @param {string} separator - Separator string
11535
+ * @returns {string} Joined string
11536
+ */
11537
+ join(array, separator) {
11538
+ if (!Array.isArray(array)) {
11539
+ return '';
11540
+ }
11541
+ return array.join(separator);
11542
+ },
11543
+
11544
+ /**
11545
+ * Format path for display
11546
+ *
11547
+ * @param {string} path - File path
11548
+ * @returns {string} Formatted path
11549
+ */
11550
+ format_path(path) {
11551
+ if (!path) {
11552
+ return '';
11553
+ }
11554
+ // Simple formatting: just return the path as-is for now
11555
+ // In real JJ, this might apply colors, truncation, etc.
11556
+ return path;
11557
+ },
11387
11558
  },
11388
11559
 
11389
11560
  /**