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.cjs +160 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +160 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/api/repository.js +94 -8
- package/src/core/jj-checkout.js +7 -1
- package/src/core/jj-operation-store.js +7 -1
- package/src/core/jj-tree-state.js +7 -1
- package/src/core/jj-view-store.js +7 -1
- package/src/core/tag-store.js +9 -2
- package/src/core/working-copy.js +3 -3
- package/src/utils/validation.js +26 -12
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
6240
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|