isomorphic-jj 1.1.2 → 1.2.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
  /**
@@ -4391,11 +4466,17 @@ class UserConfig {
4391
4466
  const __filename$3 = fileURLToPath(import.meta.url);
4392
4467
  const __dirname$3 = path.dirname(__filename$3);
4393
4468
 
4469
+ // Determine proto path based on whether we're running from dist or src
4470
+ const inDist$3 = __dirname$3.includes('/dist') || __dirname$3.includes('\\dist');
4471
+ const protoDir$3 = inDist$3
4472
+ ? path.join(__dirname$3, '..', 'src', 'protos')
4473
+ : path.join(__dirname$3, '..', 'protos');
4474
+
4394
4475
  class JJCheckout {
4395
4476
  constructor(fs, dir) {
4396
4477
  this.fs = fs;
4397
4478
  this.dir = dir;
4398
- this.protoPath = path.join(__dirname$3, '..', 'protos', 'local_working_copy.proto');
4479
+ this.protoPath = path.join(protoDir$3, 'local_working_copy.proto');
4399
4480
  }
4400
4481
 
4401
4482
  /**
@@ -4467,11 +4548,17 @@ class JJCheckout {
4467
4548
  const __filename$2 = fileURLToPath(import.meta.url);
4468
4549
  const __dirname$2 = path.dirname(__filename$2);
4469
4550
 
4551
+ // Determine proto path based on whether we're running from dist or src
4552
+ const inDist$2 = __dirname$2.includes('/dist') || __dirname$2.includes('\\dist');
4553
+ const protoDir$2 = inDist$2
4554
+ ? path.join(__dirname$2, '..', 'src', 'protos')
4555
+ : path.join(__dirname$2, '..', 'protos');
4556
+
4470
4557
  class JJTreeState {
4471
4558
  constructor(fs, dir) {
4472
4559
  this.fs = fs;
4473
4560
  this.dir = dir;
4474
- this.protoPath = path.join(__dirname$2, '..', 'protos', 'local_working_copy.proto');
4561
+ this.protoPath = path.join(protoDir$2, 'local_working_copy.proto');
4475
4562
  }
4476
4563
 
4477
4564
  /**
@@ -4559,11 +4646,17 @@ class JJTreeState {
4559
4646
  const __filename$1 = fileURLToPath(import.meta.url);
4560
4647
  const __dirname$1 = path.dirname(__filename$1);
4561
4648
 
4649
+ // Determine proto path based on whether we're running from dist or src
4650
+ const inDist$1 = __dirname$1.includes('/dist') || __dirname$1.includes('\\dist');
4651
+ const protoDir$1 = inDist$1
4652
+ ? path.join(__dirname$1, '..', 'src', 'protos')
4653
+ : path.join(__dirname$1, '..', 'protos');
4654
+
4562
4655
  class JJOperationStore {
4563
4656
  constructor(fs, dir) {
4564
4657
  this.fs = fs;
4565
4658
  this.dir = dir;
4566
- this.protoPath = path.join(__dirname$1, '..', 'protos', 'simple_op_store.proto');
4659
+ this.protoPath = path.join(protoDir$1, 'simple_op_store.proto');
4567
4660
  }
4568
4661
 
4569
4662
  /**
@@ -4677,11 +4770,17 @@ class JJOperationStore {
4677
4770
  const __filename = fileURLToPath(import.meta.url);
4678
4771
  const __dirname = path.dirname(__filename);
4679
4772
 
4773
+ // Determine proto path based on whether we're running from dist or src
4774
+ const inDist = __dirname.includes('/dist') || __dirname.includes('\\dist');
4775
+ const protoDir = inDist
4776
+ ? path.join(__dirname, '..', 'src', 'protos')
4777
+ : path.join(__dirname, '..', 'protos');
4778
+
4680
4779
  class JJViewStore {
4681
4780
  constructor(fs, dir) {
4682
4781
  this.fs = fs;
4683
4782
  this.dir = dir;
4684
- this.protoPath = path.join(__dirname, '..', 'protos', 'simple_op_store.proto');
4783
+ this.protoPath = path.join(protoDir, 'simple_op_store.proto');
4685
4784
  }
4686
4785
 
4687
4786
  /**
@@ -6990,7 +7089,8 @@ async function createJJ(options) {
6990
7089
  * @param {Object} args - Arguments
6991
7090
  * @param {string} [args.revset='all()'] - Revset expression to filter changes
6992
7091
  * @param {number} [args.limit] - Maximum number of changes to return
6993
- * @returns {Promise<Array>} Array of changes
7092
+ * @param {boolean} [args.count=false] - If true, return count instead of changes (v0.36.0)
7093
+ * @returns {Promise<Array|number>} Array of changes or count if args.count is true
6994
7094
  */
6995
7095
  async log(args = {}) {
6996
7096
  await graph.load();
@@ -6999,6 +7099,11 @@ async function createJJ(options) {
6999
7099
  const revsetExpr = args.revset || 'all()';
7000
7100
  const changeIds = await revset.evaluate(revsetExpr);
7001
7101
 
7102
+ // If count mode, return count early (v0.36.0 feature)
7103
+ if (args.count) {
7104
+ return changeIds.length;
7105
+ }
7106
+
7002
7107
  let changes = [];
7003
7108
  for (const changeId of changeIds) {
7004
7109
  const change = await graph.getChange(changeId);
@@ -8019,9 +8124,18 @@ async function createJJ(options) {
8019
8124
  * @returns {Promise<Object>} The new backout change including changeId, description, and backedOut field
8020
8125
  */
8021
8126
  async backout(args) {
8127
+ // Normalize: accept common parameter aliases
8128
+ if (args && args.change && !args.revision) {
8129
+ args = { ...args, revision: args.change };
8130
+ } else if (args && args.changeId && !args.revision) {
8131
+ args = { ...args, revision: args.changeId };
8132
+ } else if (args && args.target && !args.revision) {
8133
+ args = { ...args, revision: args.target };
8134
+ }
8135
+
8022
8136
  if (!args || !args.revision) {
8023
8137
  throw new JJError('INVALID_ARGUMENT', 'Missing revision argument', {
8024
- suggestion: 'Provide the change ID to back out: { revision: "abc123..." }',
8138
+ suggestion: 'Provide the change ID to back out: { revision: "abc123..." } (or use change/changeId/target)',
8025
8139
  });
8026
8140
  }
8027
8141
 
@@ -9056,6 +9170,15 @@ async function createJJ(options) {
9056
9170
  * @param {string[]} [args.paths] - Specific paths to include in first split (full implementation planned for future)
9057
9171
  */
9058
9172
  async split(args) {
9173
+ // Normalize: accept common parameter aliases
9174
+ if (args && args.change && !args.changeId) {
9175
+ args = { ...args, changeId: args.change };
9176
+ } else if (args && args.revision && !args.changeId) {
9177
+ args = { ...args, changeId: args.revision };
9178
+ } else if (args && args.target && !args.changeId) {
9179
+ args = { ...args, changeId: args.target };
9180
+ }
9181
+
9059
9182
  // Check for interactive mode (not supported)
9060
9183
  if (args.interactive) {
9061
9184
  throw new JJError(
@@ -10767,9 +10890,21 @@ async function createJJ(options) {
10767
10890
  * @returns {Promise<Object>} Bookmark info
10768
10891
  */
10769
10892
  async set(args) {
10893
+ // Normalize: accept common parameter aliases
10894
+ if (args) {
10895
+ if (args.target && !args.changeId) {
10896
+ args = { ...args, changeId: args.target };
10897
+ } else if (args.change && !args.changeId) {
10898
+ args = { ...args, changeId: args.change };
10899
+ } else if (args.revision && !args.changeId) {
10900
+ args = { ...args, changeId: args.revision };
10901
+ }
10902
+ }
10903
+
10770
10904
  if (!args || !args.name || !args.changeId) {
10771
10905
  throw new JJError('INVALID_ARGUMENT', 'Missing name or changeId', {
10772
- suggestion: 'Provide both: { name: "main", changeId: "abc123..." }',
10906
+ args,
10907
+ suggestion: 'Provide both: { name: "main", changeId: "abc123..." } (or use target/change/revision instead of changeId)',
10773
10908
  });
10774
10909
  }
10775
10910
 
@@ -10805,6 +10940,17 @@ async function createJJ(options) {
10805
10940
  * @returns {Promise<Object>} Bookmark info
10806
10941
  */
10807
10942
  async create(args) {
10943
+ // Normalize: accept common parameter aliases
10944
+ if (args) {
10945
+ if (args.target && !args.changeId) {
10946
+ args = { ...args, changeId: args.target };
10947
+ } else if (args.change && !args.changeId) {
10948
+ args = { ...args, changeId: args.change };
10949
+ } else if (args.revision && !args.changeId) {
10950
+ args = { ...args, changeId: args.revision };
10951
+ }
10952
+ }
10953
+
10808
10954
  if (!args || !args.name) {
10809
10955
  throw new JJError('INVALID_ARGUMENT', 'Missing name argument', {
10810
10956
  suggestion: 'Provide: { name: "main", changeId: "abc123..." } or { name: "main" } for working copy',
@@ -10857,9 +11003,23 @@ async function createJJ(options) {
10857
11003
  * @returns {Promise<Object>} Move result
10858
11004
  */
10859
11005
  async move(args) {
11006
+ // Normalize: accept common parameter aliases
11007
+ if (args) {
11008
+ if (args.target && !args.to) {
11009
+ args = { ...args, to: args.target };
11010
+ } else if (args.changeId && !args.to) {
11011
+ args = { ...args, to: args.changeId };
11012
+ } else if (args.change && !args.to) {
11013
+ args = { ...args, to: args.change };
11014
+ } else if (args.revision && !args.to) {
11015
+ args = { ...args, to: args.revision };
11016
+ }
11017
+ }
11018
+
10860
11019
  if (!args || !args.name || !args.to) {
10861
11020
  throw new JJError('INVALID_ARGUMENT', 'Missing name or to', {
10862
- suggestion: 'Provide both: { name: "main", to: "abc123..." }',
11021
+ args,
11022
+ suggestion: 'Provide both: { name: "main", to: "abc123..." } (or use target/changeId/change/revision instead of to)',
10863
11023
  });
10864
11024
  }
10865
11025
 
@@ -11260,21 +11420,13 @@ async function createJJ(options) {
11260
11420
  });
11261
11421
  }
11262
11422
 
11263
- await userConfig.load();
11264
-
11265
- // Handle nested keys (e.g., 'user.name')
11266
- const parts = key.split('.');
11267
- let value = userConfig.config;
11268
-
11269
- for (const part of parts) {
11270
- if (value && typeof value === 'object') {
11271
- value = value[part];
11272
- } else {
11273
- return null;
11274
- }
11423
+ // Ensure config is loaded at least once
11424
+ if (!userConfig.config) {
11425
+ await userConfig.load();
11275
11426
  }
11276
11427
 
11277
- return value;
11428
+ // Use userConfig.get() to preserve programmatic config
11429
+ return userConfig.get(key);
11278
11430
  },
11279
11431
 
11280
11432
  /**
@@ -11323,6 +11475,87 @@ async function createJJ(options) {
11323
11475
  await userConfig.load();
11324
11476
  return userConfig.config;
11325
11477
  },
11478
+
11479
+ /**
11480
+ * Reload configuration (including workspace-specific config)
11481
+ * v0.35.0: Loads workspace-config.json if present
11482
+ * v0.36.0: Accepts optional config object for programmatic configuration
11483
+ *
11484
+ * @param {Object} [opts] - Optional config options
11485
+ * @param {Object} [opts.workspace] - Workspace-specific config to merge (highest priority)
11486
+ * @param {Object} [opts.override] - Config object to merge over loaded config
11487
+ * @returns {Promise<void>}
11488
+ *
11489
+ * @example
11490
+ * // Load from files only
11491
+ * await repo.config.load();
11492
+ *
11493
+ * @example
11494
+ * // Programmatic workspace config (no file I/O needed)
11495
+ * await repo.config.load({
11496
+ * workspace: { user: { email: 'test@example.com' } }
11497
+ * });
11498
+ *
11499
+ * @example
11500
+ * // Override global config programmatically
11501
+ * await repo.config.load({
11502
+ * override: { test: { value: 'override' } }
11503
+ * });
11504
+ */
11505
+ async load(opts) {
11506
+ await userConfig.load(opts);
11507
+ },
11508
+ },
11509
+
11510
+ /**
11511
+ * Template helper functions (v0.36.0)
11512
+ *
11513
+ * Utilities for displaying and formatting JJ data
11514
+ */
11515
+ template: {
11516
+ /**
11517
+ * Get files modified in a change
11518
+ *
11519
+ * @param {string} changeId - Change ID
11520
+ * @returns {Promise<string[]>} Array of file paths
11521
+ */
11522
+ async files(changeId) {
11523
+ await graph.load();
11524
+ const change = await graph.getChange(changeId);
11525
+ if (!change || !change.fileSnapshot) {
11526
+ return [];
11527
+ }
11528
+ return Object.keys(change.fileSnapshot);
11529
+ },
11530
+
11531
+ /**
11532
+ * Join array elements with a separator
11533
+ *
11534
+ * @param {string[]} array - Array of strings
11535
+ * @param {string} separator - Separator string
11536
+ * @returns {string} Joined string
11537
+ */
11538
+ join(array, separator) {
11539
+ if (!Array.isArray(array)) {
11540
+ return '';
11541
+ }
11542
+ return array.join(separator);
11543
+ },
11544
+
11545
+ /**
11546
+ * Format path for display
11547
+ *
11548
+ * @param {string} path - File path
11549
+ * @returns {string} Formatted path
11550
+ */
11551
+ format_path(path) {
11552
+ if (!path) {
11553
+ return '';
11554
+ }
11555
+ // Simple formatting: just return the path as-is for now
11556
+ // In real JJ, this might apply colors, truncation, etc.
11557
+ return path;
11558
+ },
11326
11559
  },
11327
11560
 
11328
11561
  /**