isomorphic-jj 1.1.0 → 1.1.2

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
@@ -269,10 +269,26 @@ function validateChangeId(changeId) {
269
269
  }
270
270
  }
271
271
 
272
+ /**
273
+ * Normalize a file path by stripping leading slashes
274
+ *
275
+ * @param {string} path - File path to normalize
276
+ * @returns {string} Normalized path (without leading slash)
277
+ */
278
+ function normalizePath(path) {
279
+ if (typeof path !== 'string') {
280
+ return path;
281
+ }
282
+
283
+ // Strip leading slashes for compatibility with REST APIs and absolute paths
284
+ return path.replace(/^\/+/, '');
285
+ }
286
+
272
287
  /**
273
288
  * Validate a file path
274
- *
289
+ *
275
290
  * @param {string} path - File path to validate
291
+ * @returns {string} Normalized path
276
292
  * @throws {JJError} If path is invalid or contains path traversal
277
293
  */
278
294
  function validatePath(path) {
@@ -282,27 +298,25 @@ function validatePath(path) {
282
298
  });
283
299
  }
284
300
 
285
- if (path.includes('..')) {
301
+ // Auto-normalize: strip leading slashes (common with REST APIs)
302
+ const normalizedPath = normalizePath(path);
303
+
304
+ if (normalizedPath.includes('..')) {
286
305
  throw new JJError('INVALID_PATH', 'Path traversal (..) not allowed', {
287
- path,
306
+ path: normalizedPath,
288
307
  suggestion: 'Use relative paths without .. components',
289
308
  });
290
309
  }
291
310
 
292
- if (path.startsWith('/')) {
293
- throw new JJError('INVALID_PATH', 'Absolute paths not allowed', {
294
- path,
295
- suggestion: 'Use relative paths from repository root',
296
- });
297
- }
298
-
299
- if (path.length > 4096) {
311
+ if (normalizedPath.length > 4096) {
300
312
  throw new JJError('INVALID_PATH', 'Path exceeds maximum length', {
301
- path,
313
+ path: normalizedPath,
302
314
  maxLength: 4096,
303
315
  suggestion: 'Use shorter file paths',
304
316
  });
305
317
  }
318
+
319
+ return normalizedPath;
306
320
  }
307
321
 
308
322
  /**
@@ -776,7 +790,7 @@ class WorkingCopy {
776
790
  * @param {Object} fileState - File state (mtime, size, mode, hash)
777
791
  */
778
792
  async trackFile(path, fileState) {
779
- validatePath(path);
793
+ path = validatePath(path); // Normalize path
780
794
 
781
795
  if (!this.state) {
782
796
  await this.load();
@@ -788,11 +802,11 @@ class WorkingCopy {
788
802
 
789
803
  /**
790
804
  * Untrack a file from the working copy
791
- *
805
+ *
792
806
  * @param {string} path - File path to untrack
793
807
  */
794
808
  async untrackFile(path) {
795
- validatePath(path);
809
+ path = validatePath(path); // Normalize path
796
810
 
797
811
  if (!this.state) {
798
812
  await this.load();
@@ -1579,7 +1593,7 @@ class TagStore {
1579
1593
  */
1580
1594
  async load() {
1581
1595
  try {
1582
- const data = await this.fs.readFile(this.tagsFile, 'utf8');
1596
+ const data = await this.fs.promises.readFile(this.tagsFile, 'utf8');
1583
1597
  return JSON.parse(data);
1584
1598
  } catch (error) {
1585
1599
  if (error.code === 'ENOENT') {
@@ -1595,7 +1609,14 @@ class TagStore {
1595
1609
  * @returns {Promise<void>}
1596
1610
  */
1597
1611
  async save(tags) {
1598
- await this.fs.writeFile(this.tagsFile, JSON.stringify(tags, null, 2));
1612
+ // Ensure the store directory exists
1613
+ const storeDir = `${this.jjDir}/store`;
1614
+ try {
1615
+ await this.fs.promises.mkdir(storeDir, { recursive: true });
1616
+ } catch (err) {
1617
+ // Directory might already exist, ignore
1618
+ }
1619
+ await this.fs.promises.writeFile(this.tagsFile, JSON.stringify(tags, null, 2));
1599
1620
  }
1600
1621
 
1601
1622
  /**
@@ -6239,8 +6260,9 @@ async function createJJ(options) {
6239
6260
  async _moveChange(args) {
6240
6261
  await graph.load();
6241
6262
 
6242
- const changeId = args.changeId || args.from;
6243
- const newParent = args.newParent || args.to;
6263
+ // Support multiple parameter names for flexibility
6264
+ const changeId = args.changeId || args.from || args.source;
6265
+ const newParent = args.newParent || args.to || args.destination;
6244
6266
 
6245
6267
  if (!changeId || typeof changeId !== 'string') {
6246
6268
  throw new JJError('INVALID_ARGUMENT', 'Missing or invalid changeId', {
@@ -6580,11 +6602,14 @@ async function createJJ(options) {
6580
6602
  /**
6581
6603
  * Edit change metadata without modifying content
6582
6604
  *
6583
- * Allows updating author, committer information, and optionally regenerating the change ID.
6605
+ * Allows updating author, committer, description, and optionally regenerating the change ID.
6584
6606
  * The file content and snapshot remain unchanged.
6585
6607
  *
6586
6608
  * @param {Object} [args={}] - Arguments
6587
6609
  * @param {string} [args.revision] - Change ID to edit (defaults to working copy @)
6610
+ * @param {string} [args.change] - Alias for revision
6611
+ * @param {string} [args.description] - Update the change description
6612
+ * @param {string} [args.message] - Alias for description
6588
6613
  * @param {Object} [args.author] - Author information to update
6589
6614
  * @param {string} [args.author.name] - Author name
6590
6615
  * @param {string} [args.author.email] - Author email
@@ -6595,6 +6620,16 @@ async function createJJ(options) {
6595
6620
  * @returns {Promise<Object>} Updated change object
6596
6621
  */
6597
6622
  async metaedit(args = {}) {
6623
+ // Normalize: accept 'change' as alias for 'revision'
6624
+ if (args.change && !args.revision) {
6625
+ args = { ...args, revision: args.change };
6626
+ }
6627
+
6628
+ // Normalize: accept 'message' as alias for 'description'
6629
+ if (args.message && !args.description) {
6630
+ args = { ...args, description: args.message };
6631
+ }
6632
+
6598
6633
  await graph.load();
6599
6634
  await workingCopy.load();
6600
6635
  await userConfig.load();
@@ -6612,13 +6647,14 @@ async function createJJ(options) {
6612
6647
  const hasAuthor = args.author && (args.author.name || args.author.email);
6613
6648
  const hasCommitter = args.committer && (args.committer.name || args.committer.email);
6614
6649
  const hasResetChangeId = args.resetChangeId === true;
6650
+ const hasDescription = args.description !== undefined;
6615
6651
 
6616
- if (!hasAuthor && !hasCommitter && !hasResetChangeId) {
6652
+ if (!hasAuthor && !hasCommitter && !hasResetChangeId && !hasDescription) {
6617
6653
  throw new JJError(
6618
6654
  'INVALID_ARGUMENT',
6619
6655
  'No metadata provided to update',
6620
6656
  {
6621
- suggestion: 'Provide author, committer, or resetChangeId: true',
6657
+ suggestion: 'Provide author, committer, description, or resetChangeId: true',
6622
6658
  }
6623
6659
  );
6624
6660
  }
@@ -6663,6 +6699,11 @@ async function createJJ(options) {
6663
6699
  change.committer.timestamp = new Date().toISOString();
6664
6700
  }
6665
6701
 
6702
+ // Update description if provided
6703
+ if (args.description !== undefined) {
6704
+ change.description = args.description;
6705
+ }
6706
+
6666
6707
  // Regenerate change ID if requested
6667
6708
  if (args.resetChangeId) {
6668
6709
  // For now, resetChangeId is not fully implemented as it requires complex graph operations
@@ -7096,9 +7137,14 @@ async function createJJ(options) {
7096
7137
  * @returns {Promise<Object>} Information about the edited change including changeId, description, and file count
7097
7138
  */
7098
7139
  async edit(args) {
7140
+ // Normalize: accept both 'change' and 'changeId' parameters
7141
+ if (args && args.change && !args.changeId) {
7142
+ args = { ...args, changeId: args.change };
7143
+ }
7144
+
7099
7145
  if (!args || !args.changeId) {
7100
7146
  throw new JJError('INVALID_ARGUMENT', 'Missing changeId argument', {
7101
- suggestion: 'Provide a changeId to edit',
7147
+ suggestion: 'Provide a changeId (or change) to edit',
7102
7148
  });
7103
7149
  }
7104
7150
 
@@ -8940,6 +8986,23 @@ async function createJJ(options) {
8940
8986
  * @returns {Promise<Object>} The un-abandoned change object including changeId, description, and abandoned flag (now false)
8941
8987
  */
8942
8988
  async unabandon(args) {
8989
+ // Normalize: accept both 'change' and 'changeId' parameters
8990
+ if (args && args.change && !args.changeId) {
8991
+ args = { ...args, changeId: args.change };
8992
+ }
8993
+
8994
+ if (!args || !args.changeId) {
8995
+ throw new JJError('INVALID_ARGUMENT', 'Missing changeId argument', {
8996
+ suggestion: 'Provide a changeId (or change) to unabandon',
8997
+ });
8998
+ }
8999
+
9000
+ if (typeof args.changeId !== 'string') {
9001
+ throw new JJError('INVALID_ARGUMENT', 'Change ID must be a string', {
9002
+ changeId: args.changeId,
9003
+ });
9004
+ }
9005
+
8943
9006
  await graph.load();
8944
9007
  await userConfig.load();
8945
9008
 
@@ -10304,6 +10367,13 @@ async function createJJ(options) {
10304
10367
  */
10305
10368
  async add(args) {
10306
10369
  await workspaces.load();
10370
+
10371
+ // If no changeId specified, default to current working copy change
10372
+ if (!args.changeId) {
10373
+ const currentChangeId = workingCopy.getCurrentChangeId();
10374
+ args = { ...args, changeId: currentChangeId };
10375
+ }
10376
+
10307
10377
  const workspace = await workspaces.add(args);
10308
10378
 
10309
10379
  // If a change was specified, check it out in the new workspace