isomorphic-jj 1.0.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.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
  /**
@@ -6236,8 +6257,9 @@ async function createJJ(options) {
6236
6257
  async _moveChange(args) {
6237
6258
  await graph.load();
6238
6259
 
6239
- const changeId = args.changeId || args.from;
6240
- const newParent = args.newParent || args.to;
6260
+ // Support multiple parameter names for flexibility
6261
+ const changeId = args.changeId || args.from || args.source;
6262
+ const newParent = args.newParent || args.to || args.destination;
6241
6263
 
6242
6264
  if (!changeId || typeof changeId !== 'string') {
6243
6265
  throw new JJError('INVALID_ARGUMENT', 'Missing or invalid changeId', {
@@ -6577,11 +6599,14 @@ async function createJJ(options) {
6577
6599
  /**
6578
6600
  * Edit change metadata without modifying content
6579
6601
  *
6580
- * Allows updating author, committer information, and optionally regenerating the change ID.
6602
+ * Allows updating author, committer, description, and optionally regenerating the change ID.
6581
6603
  * The file content and snapshot remain unchanged.
6582
6604
  *
6583
6605
  * @param {Object} [args={}] - Arguments
6584
6606
  * @param {string} [args.revision] - Change ID to edit (defaults to working copy @)
6607
+ * @param {string} [args.change] - Alias for revision
6608
+ * @param {string} [args.description] - Update the change description
6609
+ * @param {string} [args.message] - Alias for description
6585
6610
  * @param {Object} [args.author] - Author information to update
6586
6611
  * @param {string} [args.author.name] - Author name
6587
6612
  * @param {string} [args.author.email] - Author email
@@ -6592,6 +6617,16 @@ async function createJJ(options) {
6592
6617
  * @returns {Promise<Object>} Updated change object
6593
6618
  */
6594
6619
  async metaedit(args = {}) {
6620
+ // Normalize: accept 'change' as alias for 'revision'
6621
+ if (args.change && !args.revision) {
6622
+ args = { ...args, revision: args.change };
6623
+ }
6624
+
6625
+ // Normalize: accept 'message' as alias for 'description'
6626
+ if (args.message && !args.description) {
6627
+ args = { ...args, description: args.message };
6628
+ }
6629
+
6595
6630
  await graph.load();
6596
6631
  await workingCopy.load();
6597
6632
  await userConfig.load();
@@ -6609,13 +6644,14 @@ async function createJJ(options) {
6609
6644
  const hasAuthor = args.author && (args.author.name || args.author.email);
6610
6645
  const hasCommitter = args.committer && (args.committer.name || args.committer.email);
6611
6646
  const hasResetChangeId = args.resetChangeId === true;
6647
+ const hasDescription = args.description !== undefined;
6612
6648
 
6613
- if (!hasAuthor && !hasCommitter && !hasResetChangeId) {
6649
+ if (!hasAuthor && !hasCommitter && !hasResetChangeId && !hasDescription) {
6614
6650
  throw new JJError(
6615
6651
  'INVALID_ARGUMENT',
6616
6652
  'No metadata provided to update',
6617
6653
  {
6618
- suggestion: 'Provide author, committer, or resetChangeId: true',
6654
+ suggestion: 'Provide author, committer, description, or resetChangeId: true',
6619
6655
  }
6620
6656
  );
6621
6657
  }
@@ -6660,6 +6696,11 @@ async function createJJ(options) {
6660
6696
  change.committer.timestamp = new Date().toISOString();
6661
6697
  }
6662
6698
 
6699
+ // Update description if provided
6700
+ if (args.description !== undefined) {
6701
+ change.description = args.description;
6702
+ }
6703
+
6663
6704
  // Regenerate change ID if requested
6664
6705
  if (args.resetChangeId) {
6665
6706
  // For now, resetChangeId is not fully implemented as it requires complex graph operations
@@ -7093,9 +7134,14 @@ async function createJJ(options) {
7093
7134
  * @returns {Promise<Object>} Information about the edited change including changeId, description, and file count
7094
7135
  */
7095
7136
  async edit(args) {
7137
+ // Normalize: accept both 'change' and 'changeId' parameters
7138
+ if (args && args.change && !args.changeId) {
7139
+ args = { ...args, changeId: args.change };
7140
+ }
7141
+
7096
7142
  if (!args || !args.changeId) {
7097
7143
  throw new JJError('INVALID_ARGUMENT', 'Missing changeId argument', {
7098
- suggestion: 'Provide a changeId to edit',
7144
+ suggestion: 'Provide a changeId (or change) to edit',
7099
7145
  });
7100
7146
  }
7101
7147
 
@@ -8937,6 +8983,23 @@ async function createJJ(options) {
8937
8983
  * @returns {Promise<Object>} The un-abandoned change object including changeId, description, and abandoned flag (now false)
8938
8984
  */
8939
8985
  async unabandon(args) {
8986
+ // Normalize: accept both 'change' and 'changeId' parameters
8987
+ if (args && args.change && !args.changeId) {
8988
+ args = { ...args, changeId: args.change };
8989
+ }
8990
+
8991
+ if (!args || !args.changeId) {
8992
+ throw new JJError('INVALID_ARGUMENT', 'Missing changeId argument', {
8993
+ suggestion: 'Provide a changeId (or change) to unabandon',
8994
+ });
8995
+ }
8996
+
8997
+ if (typeof args.changeId !== 'string') {
8998
+ throw new JJError('INVALID_ARGUMENT', 'Change ID must be a string', {
8999
+ changeId: args.changeId,
9000
+ });
9001
+ }
9002
+
8940
9003
  await graph.load();
8941
9004
  await userConfig.load();
8942
9005
 
@@ -10301,6 +10364,13 @@ async function createJJ(options) {
10301
10364
  */
10302
10365
  async add(args) {
10303
10366
  await workspaces.load();
10367
+
10368
+ // If no changeId specified, default to current working copy change
10369
+ if (!args.changeId) {
10370
+ const currentChangeId = workingCopy.getCurrentChangeId();
10371
+ args = { ...args, changeId: currentChangeId };
10372
+ }
10373
+
10304
10374
  const workspace = await workspaces.add(args);
10305
10375
 
10306
10376
  // If a change was specified, check it out in the new workspace