isomorphic-jj 1.1.0 → 1.1.3

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
@@ -266,10 +266,26 @@ function validateChangeId(changeId) {
266
266
  }
267
267
  }
268
268
 
269
+ /**
270
+ * Normalize a file path by stripping leading slashes
271
+ *
272
+ * @param {string} path - File path to normalize
273
+ * @returns {string} Normalized path (without leading slash)
274
+ */
275
+ function normalizePath(path) {
276
+ if (typeof path !== 'string') {
277
+ return path;
278
+ }
279
+
280
+ // Strip leading slashes for compatibility with REST APIs and absolute paths
281
+ return path.replace(/^\/+/, '');
282
+ }
283
+
269
284
  /**
270
285
  * Validate a file path
271
- *
286
+ *
272
287
  * @param {string} path - File path to validate
288
+ * @returns {string} Normalized path
273
289
  * @throws {JJError} If path is invalid or contains path traversal
274
290
  */
275
291
  function validatePath(path) {
@@ -279,27 +295,25 @@ function validatePath(path) {
279
295
  });
280
296
  }
281
297
 
282
- if (path.includes('..')) {
298
+ // Auto-normalize: strip leading slashes (common with REST APIs)
299
+ const normalizedPath = normalizePath(path);
300
+
301
+ if (normalizedPath.includes('..')) {
283
302
  throw new JJError('INVALID_PATH', 'Path traversal (..) not allowed', {
284
- path,
303
+ path: normalizedPath,
285
304
  suggestion: 'Use relative paths without .. components',
286
305
  });
287
306
  }
288
307
 
289
- if (path.startsWith('/')) {
290
- throw new JJError('INVALID_PATH', 'Absolute paths not allowed', {
291
- path,
292
- suggestion: 'Use relative paths from repository root',
293
- });
294
- }
295
-
296
- if (path.length > 4096) {
308
+ if (normalizedPath.length > 4096) {
297
309
  throw new JJError('INVALID_PATH', 'Path exceeds maximum length', {
298
- path,
310
+ path: normalizedPath,
299
311
  maxLength: 4096,
300
312
  suggestion: 'Use shorter file paths',
301
313
  });
302
314
  }
315
+
316
+ return normalizedPath;
303
317
  }
304
318
 
305
319
  /**
@@ -773,7 +787,7 @@ class WorkingCopy {
773
787
  * @param {Object} fileState - File state (mtime, size, mode, hash)
774
788
  */
775
789
  async trackFile(path, fileState) {
776
- validatePath(path);
790
+ path = validatePath(path); // Normalize path
777
791
 
778
792
  if (!this.state) {
779
793
  await this.load();
@@ -785,11 +799,11 @@ class WorkingCopy {
785
799
 
786
800
  /**
787
801
  * Untrack a file from the working copy
788
- *
802
+ *
789
803
  * @param {string} path - File path to untrack
790
804
  */
791
805
  async untrackFile(path) {
792
- validatePath(path);
806
+ path = validatePath(path); // Normalize path
793
807
 
794
808
  if (!this.state) {
795
809
  await this.load();
@@ -1576,7 +1590,7 @@ class TagStore {
1576
1590
  */
1577
1591
  async load() {
1578
1592
  try {
1579
- const data = await this.fs.readFile(this.tagsFile, 'utf8');
1593
+ const data = await this.fs.promises.readFile(this.tagsFile, 'utf8');
1580
1594
  return JSON.parse(data);
1581
1595
  } catch (error) {
1582
1596
  if (error.code === 'ENOENT') {
@@ -1592,7 +1606,14 @@ class TagStore {
1592
1606
  * @returns {Promise<void>}
1593
1607
  */
1594
1608
  async save(tags) {
1595
- await this.fs.writeFile(this.tagsFile, JSON.stringify(tags, null, 2));
1609
+ // Ensure the store directory exists
1610
+ const storeDir = `${this.jjDir}/store`;
1611
+ try {
1612
+ await this.fs.promises.mkdir(storeDir, { recursive: true });
1613
+ } catch (err) {
1614
+ // Directory might already exist, ignore
1615
+ }
1616
+ await this.fs.promises.writeFile(this.tagsFile, JSON.stringify(tags, null, 2));
1596
1617
  }
1597
1618
 
1598
1619
  /**
@@ -4370,11 +4391,17 @@ class UserConfig {
4370
4391
  const __filename$3 = fileURLToPath(import.meta.url);
4371
4392
  const __dirname$3 = path.dirname(__filename$3);
4372
4393
 
4394
+ // Determine proto path based on whether we're running from dist or src
4395
+ const inDist$3 = __dirname$3.includes('/dist') || __dirname$3.includes('\\dist');
4396
+ const protoDir$3 = inDist$3
4397
+ ? path.join(__dirname$3, '..', 'src', 'protos')
4398
+ : path.join(__dirname$3, '..', 'protos');
4399
+
4373
4400
  class JJCheckout {
4374
4401
  constructor(fs, dir) {
4375
4402
  this.fs = fs;
4376
4403
  this.dir = dir;
4377
- this.protoPath = path.join(__dirname$3, '..', 'protos', 'local_working_copy.proto');
4404
+ this.protoPath = path.join(protoDir$3, 'local_working_copy.proto');
4378
4405
  }
4379
4406
 
4380
4407
  /**
@@ -4446,11 +4473,17 @@ class JJCheckout {
4446
4473
  const __filename$2 = fileURLToPath(import.meta.url);
4447
4474
  const __dirname$2 = path.dirname(__filename$2);
4448
4475
 
4476
+ // Determine proto path based on whether we're running from dist or src
4477
+ const inDist$2 = __dirname$2.includes('/dist') || __dirname$2.includes('\\dist');
4478
+ const protoDir$2 = inDist$2
4479
+ ? path.join(__dirname$2, '..', 'src', 'protos')
4480
+ : path.join(__dirname$2, '..', 'protos');
4481
+
4449
4482
  class JJTreeState {
4450
4483
  constructor(fs, dir) {
4451
4484
  this.fs = fs;
4452
4485
  this.dir = dir;
4453
- this.protoPath = path.join(__dirname$2, '..', 'protos', 'local_working_copy.proto');
4486
+ this.protoPath = path.join(protoDir$2, 'local_working_copy.proto');
4454
4487
  }
4455
4488
 
4456
4489
  /**
@@ -4538,11 +4571,17 @@ class JJTreeState {
4538
4571
  const __filename$1 = fileURLToPath(import.meta.url);
4539
4572
  const __dirname$1 = path.dirname(__filename$1);
4540
4573
 
4574
+ // Determine proto path based on whether we're running from dist or src
4575
+ const inDist$1 = __dirname$1.includes('/dist') || __dirname$1.includes('\\dist');
4576
+ const protoDir$1 = inDist$1
4577
+ ? path.join(__dirname$1, '..', 'src', 'protos')
4578
+ : path.join(__dirname$1, '..', 'protos');
4579
+
4541
4580
  class JJOperationStore {
4542
4581
  constructor(fs, dir) {
4543
4582
  this.fs = fs;
4544
4583
  this.dir = dir;
4545
- this.protoPath = path.join(__dirname$1, '..', 'protos', 'simple_op_store.proto');
4584
+ this.protoPath = path.join(protoDir$1, 'simple_op_store.proto');
4546
4585
  }
4547
4586
 
4548
4587
  /**
@@ -4656,11 +4695,17 @@ class JJOperationStore {
4656
4695
  const __filename = fileURLToPath(import.meta.url);
4657
4696
  const __dirname = path.dirname(__filename);
4658
4697
 
4698
+ // Determine proto path based on whether we're running from dist or src
4699
+ const inDist = __dirname.includes('/dist') || __dirname.includes('\\dist');
4700
+ const protoDir = inDist
4701
+ ? path.join(__dirname, '..', 'src', 'protos')
4702
+ : path.join(__dirname, '..', 'protos');
4703
+
4659
4704
  class JJViewStore {
4660
4705
  constructor(fs, dir) {
4661
4706
  this.fs = fs;
4662
4707
  this.dir = dir;
4663
- this.protoPath = path.join(__dirname, '..', 'protos', 'simple_op_store.proto');
4708
+ this.protoPath = path.join(protoDir, 'simple_op_store.proto');
4664
4709
  }
4665
4710
 
4666
4711
  /**
@@ -6236,8 +6281,9 @@ async function createJJ(options) {
6236
6281
  async _moveChange(args) {
6237
6282
  await graph.load();
6238
6283
 
6239
- const changeId = args.changeId || args.from;
6240
- const newParent = args.newParent || args.to;
6284
+ // Support multiple parameter names for flexibility
6285
+ const changeId = args.changeId || args.from || args.source;
6286
+ const newParent = args.newParent || args.to || args.destination;
6241
6287
 
6242
6288
  if (!changeId || typeof changeId !== 'string') {
6243
6289
  throw new JJError('INVALID_ARGUMENT', 'Missing or invalid changeId', {
@@ -6577,11 +6623,14 @@ async function createJJ(options) {
6577
6623
  /**
6578
6624
  * Edit change metadata without modifying content
6579
6625
  *
6580
- * Allows updating author, committer information, and optionally regenerating the change ID.
6626
+ * Allows updating author, committer, description, and optionally regenerating the change ID.
6581
6627
  * The file content and snapshot remain unchanged.
6582
6628
  *
6583
6629
  * @param {Object} [args={}] - Arguments
6584
6630
  * @param {string} [args.revision] - Change ID to edit (defaults to working copy @)
6631
+ * @param {string} [args.change] - Alias for revision
6632
+ * @param {string} [args.description] - Update the change description
6633
+ * @param {string} [args.message] - Alias for description
6585
6634
  * @param {Object} [args.author] - Author information to update
6586
6635
  * @param {string} [args.author.name] - Author name
6587
6636
  * @param {string} [args.author.email] - Author email
@@ -6592,6 +6641,16 @@ async function createJJ(options) {
6592
6641
  * @returns {Promise<Object>} Updated change object
6593
6642
  */
6594
6643
  async metaedit(args = {}) {
6644
+ // Normalize: accept 'change' as alias for 'revision'
6645
+ if (args.change && !args.revision) {
6646
+ args = { ...args, revision: args.change };
6647
+ }
6648
+
6649
+ // Normalize: accept 'message' as alias for 'description'
6650
+ if (args.message && !args.description) {
6651
+ args = { ...args, description: args.message };
6652
+ }
6653
+
6595
6654
  await graph.load();
6596
6655
  await workingCopy.load();
6597
6656
  await userConfig.load();
@@ -6609,13 +6668,14 @@ async function createJJ(options) {
6609
6668
  const hasAuthor = args.author && (args.author.name || args.author.email);
6610
6669
  const hasCommitter = args.committer && (args.committer.name || args.committer.email);
6611
6670
  const hasResetChangeId = args.resetChangeId === true;
6671
+ const hasDescription = args.description !== undefined;
6612
6672
 
6613
- if (!hasAuthor && !hasCommitter && !hasResetChangeId) {
6673
+ if (!hasAuthor && !hasCommitter && !hasResetChangeId && !hasDescription) {
6614
6674
  throw new JJError(
6615
6675
  'INVALID_ARGUMENT',
6616
6676
  'No metadata provided to update',
6617
6677
  {
6618
- suggestion: 'Provide author, committer, or resetChangeId: true',
6678
+ suggestion: 'Provide author, committer, description, or resetChangeId: true',
6619
6679
  }
6620
6680
  );
6621
6681
  }
@@ -6660,6 +6720,11 @@ async function createJJ(options) {
6660
6720
  change.committer.timestamp = new Date().toISOString();
6661
6721
  }
6662
6722
 
6723
+ // Update description if provided
6724
+ if (args.description !== undefined) {
6725
+ change.description = args.description;
6726
+ }
6727
+
6663
6728
  // Regenerate change ID if requested
6664
6729
  if (args.resetChangeId) {
6665
6730
  // For now, resetChangeId is not fully implemented as it requires complex graph operations
@@ -7093,9 +7158,14 @@ async function createJJ(options) {
7093
7158
  * @returns {Promise<Object>} Information about the edited change including changeId, description, and file count
7094
7159
  */
7095
7160
  async edit(args) {
7161
+ // Normalize: accept both 'change' and 'changeId' parameters
7162
+ if (args && args.change && !args.changeId) {
7163
+ args = { ...args, changeId: args.change };
7164
+ }
7165
+
7096
7166
  if (!args || !args.changeId) {
7097
7167
  throw new JJError('INVALID_ARGUMENT', 'Missing changeId argument', {
7098
- suggestion: 'Provide a changeId to edit',
7168
+ suggestion: 'Provide a changeId (or change) to edit',
7099
7169
  });
7100
7170
  }
7101
7171
 
@@ -8937,6 +9007,23 @@ async function createJJ(options) {
8937
9007
  * @returns {Promise<Object>} The un-abandoned change object including changeId, description, and abandoned flag (now false)
8938
9008
  */
8939
9009
  async unabandon(args) {
9010
+ // Normalize: accept both 'change' and 'changeId' parameters
9011
+ if (args && args.change && !args.changeId) {
9012
+ args = { ...args, changeId: args.change };
9013
+ }
9014
+
9015
+ if (!args || !args.changeId) {
9016
+ throw new JJError('INVALID_ARGUMENT', 'Missing changeId argument', {
9017
+ suggestion: 'Provide a changeId (or change) to unabandon',
9018
+ });
9019
+ }
9020
+
9021
+ if (typeof args.changeId !== 'string') {
9022
+ throw new JJError('INVALID_ARGUMENT', 'Change ID must be a string', {
9023
+ changeId: args.changeId,
9024
+ });
9025
+ }
9026
+
8940
9027
  await graph.load();
8941
9028
  await userConfig.load();
8942
9029
 
@@ -10301,6 +10388,13 @@ async function createJJ(options) {
10301
10388
  */
10302
10389
  async add(args) {
10303
10390
  await workspaces.load();
10391
+
10392
+ // If no changeId specified, default to current working copy change
10393
+ if (!args.changeId) {
10394
+ const currentChangeId = workingCopy.getCurrentChangeId();
10395
+ args = { ...args, changeId: currentChangeId };
10396
+ }
10397
+
10304
10398
  const workspace = await workspaces.add(args);
10305
10399
 
10306
10400
  // If a change was specified, check it out in the new workspace
@@ -10697,9 +10791,21 @@ async function createJJ(options) {
10697
10791
  * @returns {Promise<Object>} Bookmark info
10698
10792
  */
10699
10793
  async set(args) {
10794
+ // Normalize: accept common parameter aliases
10795
+ if (args) {
10796
+ if (args.target && !args.changeId) {
10797
+ args = { ...args, changeId: args.target };
10798
+ } else if (args.change && !args.changeId) {
10799
+ args = { ...args, changeId: args.change };
10800
+ } else if (args.revision && !args.changeId) {
10801
+ args = { ...args, changeId: args.revision };
10802
+ }
10803
+ }
10804
+
10700
10805
  if (!args || !args.name || !args.changeId) {
10701
10806
  throw new JJError('INVALID_ARGUMENT', 'Missing name or changeId', {
10702
- suggestion: 'Provide both: { name: "main", changeId: "abc123..." }',
10807
+ args,
10808
+ suggestion: 'Provide both: { name: "main", changeId: "abc123..." } (or use target/change/revision instead of changeId)',
10703
10809
  });
10704
10810
  }
10705
10811
 
@@ -10735,6 +10841,17 @@ async function createJJ(options) {
10735
10841
  * @returns {Promise<Object>} Bookmark info
10736
10842
  */
10737
10843
  async create(args) {
10844
+ // Normalize: accept common parameter aliases
10845
+ if (args) {
10846
+ if (args.target && !args.changeId) {
10847
+ args = { ...args, changeId: args.target };
10848
+ } else if (args.change && !args.changeId) {
10849
+ args = { ...args, changeId: args.change };
10850
+ } else if (args.revision && !args.changeId) {
10851
+ args = { ...args, changeId: args.revision };
10852
+ }
10853
+ }
10854
+
10738
10855
  if (!args || !args.name) {
10739
10856
  throw new JJError('INVALID_ARGUMENT', 'Missing name argument', {
10740
10857
  suggestion: 'Provide: { name: "main", changeId: "abc123..." } or { name: "main" } for working copy',
@@ -10787,9 +10904,23 @@ async function createJJ(options) {
10787
10904
  * @returns {Promise<Object>} Move result
10788
10905
  */
10789
10906
  async move(args) {
10907
+ // Normalize: accept common parameter aliases
10908
+ if (args) {
10909
+ if (args.target && !args.to) {
10910
+ args = { ...args, to: args.target };
10911
+ } else if (args.changeId && !args.to) {
10912
+ args = { ...args, to: args.changeId };
10913
+ } else if (args.change && !args.to) {
10914
+ args = { ...args, to: args.change };
10915
+ } else if (args.revision && !args.to) {
10916
+ args = { ...args, to: args.revision };
10917
+ }
10918
+ }
10919
+
10790
10920
  if (!args || !args.name || !args.to) {
10791
10921
  throw new JJError('INVALID_ARGUMENT', 'Missing name or to', {
10792
- suggestion: 'Provide both: { name: "main", to: "abc123..." }',
10922
+ args,
10923
+ suggestion: 'Provide both: { name: "main", to: "abc123..." } (or use target/changeId/change/revision instead of to)',
10793
10924
  });
10794
10925
  }
10795
10926