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