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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isomorphic-jj",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "A pure-JavaScript library that brings Jujutsu (jj) version control semantics to Node.js and browsers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -821,8 +821,9 @@ export async function createJJ(options) {
821
821
  async _moveChange(args) {
822
822
  await graph.load();
823
823
 
824
- const changeId = args.changeId || args.from;
825
- const newParent = args.newParent || args.to;
824
+ // Support multiple parameter names for flexibility
825
+ const changeId = args.changeId || args.from || args.source;
826
+ const newParent = args.newParent || args.to || args.destination;
826
827
 
827
828
  if (!changeId || typeof changeId !== 'string') {
828
829
  throw new JJError('INVALID_ARGUMENT', 'Missing or invalid changeId', {
@@ -1162,11 +1163,14 @@ export async function createJJ(options) {
1162
1163
  /**
1163
1164
  * Edit change metadata without modifying content
1164
1165
  *
1165
- * Allows updating author, committer information, and optionally regenerating the change ID.
1166
+ * Allows updating author, committer, description, and optionally regenerating the change ID.
1166
1167
  * The file content and snapshot remain unchanged.
1167
1168
  *
1168
1169
  * @param {Object} [args={}] - Arguments
1169
1170
  * @param {string} [args.revision] - Change ID to edit (defaults to working copy @)
1171
+ * @param {string} [args.change] - Alias for revision
1172
+ * @param {string} [args.description] - Update the change description
1173
+ * @param {string} [args.message] - Alias for description
1170
1174
  * @param {Object} [args.author] - Author information to update
1171
1175
  * @param {string} [args.author.name] - Author name
1172
1176
  * @param {string} [args.author.email] - Author email
@@ -1177,6 +1181,16 @@ export async function createJJ(options) {
1177
1181
  * @returns {Promise<Object>} Updated change object
1178
1182
  */
1179
1183
  async metaedit(args = {}) {
1184
+ // Normalize: accept 'change' as alias for 'revision'
1185
+ if (args.change && !args.revision) {
1186
+ args = { ...args, revision: args.change };
1187
+ }
1188
+
1189
+ // Normalize: accept 'message' as alias for 'description'
1190
+ if (args.message && !args.description) {
1191
+ args = { ...args, description: args.message };
1192
+ }
1193
+
1180
1194
  await graph.load();
1181
1195
  await workingCopy.load();
1182
1196
  await userConfig.load();
@@ -1194,13 +1208,14 @@ export async function createJJ(options) {
1194
1208
  const hasAuthor = args.author && (args.author.name || args.author.email);
1195
1209
  const hasCommitter = args.committer && (args.committer.name || args.committer.email);
1196
1210
  const hasResetChangeId = args.resetChangeId === true;
1211
+ const hasDescription = args.description !== undefined;
1197
1212
 
1198
- if (!hasAuthor && !hasCommitter && !hasResetChangeId) {
1213
+ if (!hasAuthor && !hasCommitter && !hasResetChangeId && !hasDescription) {
1199
1214
  throw new JJError(
1200
1215
  'INVALID_ARGUMENT',
1201
1216
  'No metadata provided to update',
1202
1217
  {
1203
- suggestion: 'Provide author, committer, or resetChangeId: true',
1218
+ suggestion: 'Provide author, committer, description, or resetChangeId: true',
1204
1219
  }
1205
1220
  );
1206
1221
  }
@@ -1245,6 +1260,11 @@ export async function createJJ(options) {
1245
1260
  change.committer.timestamp = new Date().toISOString();
1246
1261
  }
1247
1262
 
1263
+ // Update description if provided
1264
+ if (args.description !== undefined) {
1265
+ change.description = args.description;
1266
+ }
1267
+
1248
1268
  // Regenerate change ID if requested
1249
1269
  if (args.resetChangeId) {
1250
1270
  // For now, resetChangeId is not fully implemented as it requires complex graph operations
@@ -1678,9 +1698,14 @@ export async function createJJ(options) {
1678
1698
  * @returns {Promise<Object>} Information about the edited change including changeId, description, and file count
1679
1699
  */
1680
1700
  async edit(args) {
1701
+ // Normalize: accept both 'change' and 'changeId' parameters
1702
+ if (args && args.change && !args.changeId) {
1703
+ args = { ...args, changeId: args.change };
1704
+ }
1705
+
1681
1706
  if (!args || !args.changeId) {
1682
1707
  throw new JJError('INVALID_ARGUMENT', 'Missing changeId argument', {
1683
- suggestion: 'Provide a changeId to edit',
1708
+ suggestion: 'Provide a changeId (or change) to edit',
1684
1709
  });
1685
1710
  }
1686
1711
 
@@ -3522,6 +3547,23 @@ export async function createJJ(options) {
3522
3547
  * @returns {Promise<Object>} The un-abandoned change object including changeId, description, and abandoned flag (now false)
3523
3548
  */
3524
3549
  async unabandon(args) {
3550
+ // Normalize: accept both 'change' and 'changeId' parameters
3551
+ if (args && args.change && !args.changeId) {
3552
+ args = { ...args, changeId: args.change };
3553
+ }
3554
+
3555
+ if (!args || !args.changeId) {
3556
+ throw new JJError('INVALID_ARGUMENT', 'Missing changeId argument', {
3557
+ suggestion: 'Provide a changeId (or change) to unabandon',
3558
+ });
3559
+ }
3560
+
3561
+ if (typeof args.changeId !== 'string') {
3562
+ throw new JJError('INVALID_ARGUMENT', 'Change ID must be a string', {
3563
+ changeId: args.changeId,
3564
+ });
3565
+ }
3566
+
3525
3567
  await graph.load();
3526
3568
  await userConfig.load();
3527
3569
 
@@ -4886,6 +4928,13 @@ export async function createJJ(options) {
4886
4928
  */
4887
4929
  async add(args) {
4888
4930
  await workspaces.load();
4931
+
4932
+ // If no changeId specified, default to current working copy change
4933
+ if (!args.changeId) {
4934
+ const currentChangeId = workingCopy.getCurrentChangeId();
4935
+ args = { ...args, changeId: currentChangeId };
4936
+ }
4937
+
4889
4938
  const workspace = await workspaces.add(args);
4890
4939
 
4891
4940
  // If a change was specified, check it out in the new workspace
@@ -64,7 +64,7 @@ export class TagStore {
64
64
  */
65
65
  async load() {
66
66
  try {
67
- const data = await this.fs.readFile(this.tagsFile, 'utf8');
67
+ const data = await this.fs.promises.readFile(this.tagsFile, 'utf8');
68
68
  return JSON.parse(data);
69
69
  } catch (error) {
70
70
  if (error.code === 'ENOENT') {
@@ -80,7 +80,14 @@ export class TagStore {
80
80
  * @returns {Promise<void>}
81
81
  */
82
82
  async save(tags) {
83
- await this.fs.writeFile(this.tagsFile, JSON.stringify(tags, null, 2));
83
+ // Ensure the store directory exists
84
+ const storeDir = `${this.jjDir}/store`;
85
+ try {
86
+ await this.fs.promises.mkdir(storeDir, { recursive: true });
87
+ } catch (err) {
88
+ // Directory might already exist, ignore
89
+ }
90
+ await this.fs.promises.writeFile(this.tagsFile, JSON.stringify(tags, null, 2));
84
91
  }
85
92
 
86
93
  /**
@@ -132,7 +132,7 @@ export class WorkingCopy {
132
132
  * @param {Object} fileState - File state (mtime, size, mode, hash)
133
133
  */
134
134
  async trackFile(path, fileState) {
135
- validatePath(path);
135
+ path = validatePath(path); // Normalize path
136
136
 
137
137
  if (!this.state) {
138
138
  await this.load();
@@ -144,11 +144,11 @@ export class WorkingCopy {
144
144
 
145
145
  /**
146
146
  * Untrack a file from the working copy
147
- *
147
+ *
148
148
  * @param {string} path - File path to untrack
149
149
  */
150
150
  async untrackFile(path) {
151
- validatePath(path);
151
+ path = validatePath(path); // Normalize path
152
152
 
153
153
  if (!this.state) {
154
154
  await this.load();
@@ -32,10 +32,26 @@ export function validateChangeId(changeId) {
32
32
  }
33
33
  }
34
34
 
35
+ /**
36
+ * Normalize a file path by stripping leading slashes
37
+ *
38
+ * @param {string} path - File path to normalize
39
+ * @returns {string} Normalized path (without leading slash)
40
+ */
41
+ export function normalizePath(path) {
42
+ if (typeof path !== 'string') {
43
+ return path;
44
+ }
45
+
46
+ // Strip leading slashes for compatibility with REST APIs and absolute paths
47
+ return path.replace(/^\/+/, '');
48
+ }
49
+
35
50
  /**
36
51
  * Validate a file path
37
- *
52
+ *
38
53
  * @param {string} path - File path to validate
54
+ * @returns {string} Normalized path
39
55
  * @throws {JJError} If path is invalid or contains path traversal
40
56
  */
41
57
  export function validatePath(path) {
@@ -45,27 +61,25 @@ export function validatePath(path) {
45
61
  });
46
62
  }
47
63
 
48
- if (path.includes('..')) {
64
+ // Auto-normalize: strip leading slashes (common with REST APIs)
65
+ const normalizedPath = normalizePath(path);
66
+
67
+ if (normalizedPath.includes('..')) {
49
68
  throw new JJError('INVALID_PATH', 'Path traversal (..) not allowed', {
50
- path,
69
+ path: normalizedPath,
51
70
  suggestion: 'Use relative paths without .. components',
52
71
  });
53
72
  }
54
73
 
55
- if (path.startsWith('/')) {
56
- throw new JJError('INVALID_PATH', 'Absolute paths not allowed', {
57
- path,
58
- suggestion: 'Use relative paths from repository root',
59
- });
60
- }
61
-
62
- if (path.length > 4096) {
74
+ if (normalizedPath.length > 4096) {
63
75
  throw new JJError('INVALID_PATH', 'Path exceeds maximum length', {
64
- path,
76
+ path: normalizedPath,
65
77
  maxLength: 4096,
66
78
  suggestion: 'Use shorter file paths',
67
79
  });
68
80
  }
81
+
82
+ return normalizedPath;
69
83
  }
70
84
 
71
85
  /**