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/README.md CHANGED
@@ -202,6 +202,54 @@ await jj.amend();
202
202
  await jj.undo();
203
203
  ```
204
204
 
205
+ ### Understanding Working Copy vs Parent
206
+
207
+ A common source of confusion: **the working copy and its parent are different changes**.
208
+
209
+ ```javascript
210
+ // After init, you have a working copy change (with changeId)
211
+ const status1 = await jj.status();
212
+ console.log(status1.workingCopy.changeId); // e.g., "abc123..."
213
+ console.log(status1.workingCopy.description); // "(no description set)"
214
+
215
+ // When you describe(), you're updating the PARENT change
216
+ await jj.describe({ message: 'Add feature' });
217
+
218
+ // The working copy changeId is STILL the same!
219
+ const status2 = await jj.status();
220
+ console.log(status2.workingCopy.changeId); // SAME: "abc123..."
221
+ console.log(status2.workingCopy.description); // UPDATED: "Add feature"
222
+
223
+ // When you new(), you create a NEW working copy
224
+ await jj.new({ message: 'Next change' });
225
+
226
+ const status3 = await jj.status();
227
+ console.log(status3.workingCopy.changeId); // DIFFERENT: "def456..."
228
+ console.log(status3.workingCopy.description); // "Next change"
229
+ ```
230
+
231
+ **Key points:**
232
+ - **Working copy IS a change** with its own `changeId` (always `@` in revsets)
233
+ - **`describe()`** updates the parent's description, not the working copy's changeId
234
+ - **`status()`** returns the working copy changeId (which may or may not have a description)
235
+ - **`new()`** creates a new working copy change and makes the previous one the parent
236
+ - **Reading metadata immediately after write** shows working copy state, not parent state
237
+
238
+ **Common pattern:**
239
+ ```javascript
240
+ // 1. Edit files
241
+ await jj.write({ path: 'file.js', data: 'content' });
242
+
243
+ // 2. Describe the change (updates parent)
244
+ await jj.describe({ message: 'Add file' });
245
+
246
+ // 3. Create new working copy for next change
247
+ await jj.new({ message: 'Next feature' });
248
+
249
+ // Or use commit() to combine describe() + new()
250
+ await jj.commit({ message: 'Add file', nextMessage: 'Next feature' });
251
+ ```
252
+
205
253
  ---
206
254
 
207
255
  ## Features
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
  /**
@@ -4394,11 +4469,17 @@ class UserConfig {
4394
4469
  const __filename$4 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
4395
4470
  const __dirname$4 = path.dirname(__filename$4);
4396
4471
 
4472
+ // Determine proto path based on whether we're running from dist or src
4473
+ const inDist$3 = __dirname$4.includes('/dist') || __dirname$4.includes('\\dist');
4474
+ const protoDir$3 = inDist$3
4475
+ ? path.join(__dirname$4, '..', 'src', 'protos')
4476
+ : path.join(__dirname$4, '..', 'protos');
4477
+
4397
4478
  class JJCheckout {
4398
4479
  constructor(fs, dir) {
4399
4480
  this.fs = fs;
4400
4481
  this.dir = dir;
4401
- this.protoPath = path.join(__dirname$4, '..', 'protos', 'local_working_copy.proto');
4482
+ this.protoPath = path.join(protoDir$3, 'local_working_copy.proto');
4402
4483
  }
4403
4484
 
4404
4485
  /**
@@ -4470,11 +4551,17 @@ class JJCheckout {
4470
4551
  const __filename$3 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
4471
4552
  const __dirname$3 = path.dirname(__filename$3);
4472
4553
 
4554
+ // Determine proto path based on whether we're running from dist or src
4555
+ const inDist$2 = __dirname$3.includes('/dist') || __dirname$3.includes('\\dist');
4556
+ const protoDir$2 = inDist$2
4557
+ ? path.join(__dirname$3, '..', 'src', 'protos')
4558
+ : path.join(__dirname$3, '..', 'protos');
4559
+
4473
4560
  class JJTreeState {
4474
4561
  constructor(fs, dir) {
4475
4562
  this.fs = fs;
4476
4563
  this.dir = dir;
4477
- this.protoPath = path.join(__dirname$3, '..', 'protos', 'local_working_copy.proto');
4564
+ this.protoPath = path.join(protoDir$2, 'local_working_copy.proto');
4478
4565
  }
4479
4566
 
4480
4567
  /**
@@ -4562,11 +4649,17 @@ class JJTreeState {
4562
4649
  const __filename$2 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
4563
4650
  const __dirname$2 = path.dirname(__filename$2);
4564
4651
 
4652
+ // Determine proto path based on whether we're running from dist or src
4653
+ const inDist$1 = __dirname$2.includes('/dist') || __dirname$2.includes('\\dist');
4654
+ const protoDir$1 = inDist$1
4655
+ ? path.join(__dirname$2, '..', 'src', 'protos')
4656
+ : path.join(__dirname$2, '..', 'protos');
4657
+
4565
4658
  class JJOperationStore {
4566
4659
  constructor(fs, dir) {
4567
4660
  this.fs = fs;
4568
4661
  this.dir = dir;
4569
- this.protoPath = path.join(__dirname$2, '..', 'protos', 'simple_op_store.proto');
4662
+ this.protoPath = path.join(protoDir$1, 'simple_op_store.proto');
4570
4663
  }
4571
4664
 
4572
4665
  /**
@@ -4680,11 +4773,17 @@ class JJOperationStore {
4680
4773
  const __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
4681
4774
  const __dirname$1 = path.dirname(__filename$1);
4682
4775
 
4776
+ // Determine proto path based on whether we're running from dist or src
4777
+ const inDist = __dirname$1.includes('/dist') || __dirname$1.includes('\\dist');
4778
+ const protoDir = inDist
4779
+ ? path.join(__dirname$1, '..', 'src', 'protos')
4780
+ : path.join(__dirname$1, '..', 'protos');
4781
+
4683
4782
  class JJViewStore {
4684
4783
  constructor(fs, dir) {
4685
4784
  this.fs = fs;
4686
4785
  this.dir = dir;
4687
- this.protoPath = path.join(__dirname$1, '..', 'protos', 'simple_op_store.proto');
4786
+ this.protoPath = path.join(protoDir, 'simple_op_store.proto');
4688
4787
  }
4689
4788
 
4690
4789
  /**
@@ -6993,7 +7092,8 @@ async function createJJ(options) {
6993
7092
  * @param {Object} args - Arguments
6994
7093
  * @param {string} [args.revset='all()'] - Revset expression to filter changes
6995
7094
  * @param {number} [args.limit] - Maximum number of changes to return
6996
- * @returns {Promise<Array>} Array of changes
7095
+ * @param {boolean} [args.count=false] - If true, return count instead of changes (v0.36.0)
7096
+ * @returns {Promise<Array|number>} Array of changes or count if args.count is true
6997
7097
  */
6998
7098
  async log(args = {}) {
6999
7099
  await graph.load();
@@ -7002,6 +7102,11 @@ async function createJJ(options) {
7002
7102
  const revsetExpr = args.revset || 'all()';
7003
7103
  const changeIds = await revset.evaluate(revsetExpr);
7004
7104
 
7105
+ // If count mode, return count early (v0.36.0 feature)
7106
+ if (args.count) {
7107
+ return changeIds.length;
7108
+ }
7109
+
7005
7110
  let changes = [];
7006
7111
  for (const changeId of changeIds) {
7007
7112
  const change = await graph.getChange(changeId);
@@ -8022,9 +8127,18 @@ async function createJJ(options) {
8022
8127
  * @returns {Promise<Object>} The new backout change including changeId, description, and backedOut field
8023
8128
  */
8024
8129
  async backout(args) {
8130
+ // Normalize: accept common parameter aliases
8131
+ if (args && args.change && !args.revision) {
8132
+ args = { ...args, revision: args.change };
8133
+ } else if (args && args.changeId && !args.revision) {
8134
+ args = { ...args, revision: args.changeId };
8135
+ } else if (args && args.target && !args.revision) {
8136
+ args = { ...args, revision: args.target };
8137
+ }
8138
+
8025
8139
  if (!args || !args.revision) {
8026
8140
  throw new JJError('INVALID_ARGUMENT', 'Missing revision argument', {
8027
- suggestion: 'Provide the change ID to back out: { revision: "abc123..." }',
8141
+ suggestion: 'Provide the change ID to back out: { revision: "abc123..." } (or use change/changeId/target)',
8028
8142
  });
8029
8143
  }
8030
8144
 
@@ -9059,6 +9173,15 @@ async function createJJ(options) {
9059
9173
  * @param {string[]} [args.paths] - Specific paths to include in first split (full implementation planned for future)
9060
9174
  */
9061
9175
  async split(args) {
9176
+ // Normalize: accept common parameter aliases
9177
+ if (args && args.change && !args.changeId) {
9178
+ args = { ...args, changeId: args.change };
9179
+ } else if (args && args.revision && !args.changeId) {
9180
+ args = { ...args, changeId: args.revision };
9181
+ } else if (args && args.target && !args.changeId) {
9182
+ args = { ...args, changeId: args.target };
9183
+ }
9184
+
9062
9185
  // Check for interactive mode (not supported)
9063
9186
  if (args.interactive) {
9064
9187
  throw new JJError(
@@ -10770,9 +10893,21 @@ async function createJJ(options) {
10770
10893
  * @returns {Promise<Object>} Bookmark info
10771
10894
  */
10772
10895
  async set(args) {
10896
+ // Normalize: accept common parameter aliases
10897
+ if (args) {
10898
+ if (args.target && !args.changeId) {
10899
+ args = { ...args, changeId: args.target };
10900
+ } else if (args.change && !args.changeId) {
10901
+ args = { ...args, changeId: args.change };
10902
+ } else if (args.revision && !args.changeId) {
10903
+ args = { ...args, changeId: args.revision };
10904
+ }
10905
+ }
10906
+
10773
10907
  if (!args || !args.name || !args.changeId) {
10774
10908
  throw new JJError('INVALID_ARGUMENT', 'Missing name or changeId', {
10775
- suggestion: 'Provide both: { name: "main", changeId: "abc123..." }',
10909
+ args,
10910
+ suggestion: 'Provide both: { name: "main", changeId: "abc123..." } (or use target/change/revision instead of changeId)',
10776
10911
  });
10777
10912
  }
10778
10913
 
@@ -10808,6 +10943,17 @@ async function createJJ(options) {
10808
10943
  * @returns {Promise<Object>} Bookmark info
10809
10944
  */
10810
10945
  async create(args) {
10946
+ // Normalize: accept common parameter aliases
10947
+ if (args) {
10948
+ if (args.target && !args.changeId) {
10949
+ args = { ...args, changeId: args.target };
10950
+ } else if (args.change && !args.changeId) {
10951
+ args = { ...args, changeId: args.change };
10952
+ } else if (args.revision && !args.changeId) {
10953
+ args = { ...args, changeId: args.revision };
10954
+ }
10955
+ }
10956
+
10811
10957
  if (!args || !args.name) {
10812
10958
  throw new JJError('INVALID_ARGUMENT', 'Missing name argument', {
10813
10959
  suggestion: 'Provide: { name: "main", changeId: "abc123..." } or { name: "main" } for working copy',
@@ -10860,9 +11006,23 @@ async function createJJ(options) {
10860
11006
  * @returns {Promise<Object>} Move result
10861
11007
  */
10862
11008
  async move(args) {
11009
+ // Normalize: accept common parameter aliases
11010
+ if (args) {
11011
+ if (args.target && !args.to) {
11012
+ args = { ...args, to: args.target };
11013
+ } else if (args.changeId && !args.to) {
11014
+ args = { ...args, to: args.changeId };
11015
+ } else if (args.change && !args.to) {
11016
+ args = { ...args, to: args.change };
11017
+ } else if (args.revision && !args.to) {
11018
+ args = { ...args, to: args.revision };
11019
+ }
11020
+ }
11021
+
10863
11022
  if (!args || !args.name || !args.to) {
10864
11023
  throw new JJError('INVALID_ARGUMENT', 'Missing name or to', {
10865
- suggestion: 'Provide both: { name: "main", to: "abc123..." }',
11024
+ args,
11025
+ suggestion: 'Provide both: { name: "main", to: "abc123..." } (or use target/changeId/change/revision instead of to)',
10866
11026
  });
10867
11027
  }
10868
11028
 
@@ -11263,21 +11423,13 @@ async function createJJ(options) {
11263
11423
  });
11264
11424
  }
11265
11425
 
11266
- await userConfig.load();
11267
-
11268
- // Handle nested keys (e.g., 'user.name')
11269
- const parts = key.split('.');
11270
- let value = userConfig.config;
11271
-
11272
- for (const part of parts) {
11273
- if (value && typeof value === 'object') {
11274
- value = value[part];
11275
- } else {
11276
- return null;
11277
- }
11426
+ // Ensure config is loaded at least once
11427
+ if (!userConfig.config) {
11428
+ await userConfig.load();
11278
11429
  }
11279
11430
 
11280
- return value;
11431
+ // Use userConfig.get() to preserve programmatic config
11432
+ return userConfig.get(key);
11281
11433
  },
11282
11434
 
11283
11435
  /**
@@ -11326,6 +11478,87 @@ async function createJJ(options) {
11326
11478
  await userConfig.load();
11327
11479
  return userConfig.config;
11328
11480
  },
11481
+
11482
+ /**
11483
+ * Reload configuration (including workspace-specific config)
11484
+ * v0.35.0: Loads workspace-config.json if present
11485
+ * v0.36.0: Accepts optional config object for programmatic configuration
11486
+ *
11487
+ * @param {Object} [opts] - Optional config options
11488
+ * @param {Object} [opts.workspace] - Workspace-specific config to merge (highest priority)
11489
+ * @param {Object} [opts.override] - Config object to merge over loaded config
11490
+ * @returns {Promise<void>}
11491
+ *
11492
+ * @example
11493
+ * // Load from files only
11494
+ * await repo.config.load();
11495
+ *
11496
+ * @example
11497
+ * // Programmatic workspace config (no file I/O needed)
11498
+ * await repo.config.load({
11499
+ * workspace: { user: { email: 'test@example.com' } }
11500
+ * });
11501
+ *
11502
+ * @example
11503
+ * // Override global config programmatically
11504
+ * await repo.config.load({
11505
+ * override: { test: { value: 'override' } }
11506
+ * });
11507
+ */
11508
+ async load(opts) {
11509
+ await userConfig.load(opts);
11510
+ },
11511
+ },
11512
+
11513
+ /**
11514
+ * Template helper functions (v0.36.0)
11515
+ *
11516
+ * Utilities for displaying and formatting JJ data
11517
+ */
11518
+ template: {
11519
+ /**
11520
+ * Get files modified in a change
11521
+ *
11522
+ * @param {string} changeId - Change ID
11523
+ * @returns {Promise<string[]>} Array of file paths
11524
+ */
11525
+ async files(changeId) {
11526
+ await graph.load();
11527
+ const change = await graph.getChange(changeId);
11528
+ if (!change || !change.fileSnapshot) {
11529
+ return [];
11530
+ }
11531
+ return Object.keys(change.fileSnapshot);
11532
+ },
11533
+
11534
+ /**
11535
+ * Join array elements with a separator
11536
+ *
11537
+ * @param {string[]} array - Array of strings
11538
+ * @param {string} separator - Separator string
11539
+ * @returns {string} Joined string
11540
+ */
11541
+ join(array, separator) {
11542
+ if (!Array.isArray(array)) {
11543
+ return '';
11544
+ }
11545
+ return array.join(separator);
11546
+ },
11547
+
11548
+ /**
11549
+ * Format path for display
11550
+ *
11551
+ * @param {string} path - File path
11552
+ * @returns {string} Formatted path
11553
+ */
11554
+ format_path(path) {
11555
+ if (!path) {
11556
+ return '';
11557
+ }
11558
+ // Simple formatting: just return the path as-is for now
11559
+ // In real JJ, this might apply colors, truncation, etc.
11560
+ return path;
11561
+ },
11329
11562
  },
11330
11563
 
11331
11564
  /**