isomorphic-jj 1.4.3 → 1.7.0
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/CHANGELOG.md +226 -0
- package/README.md +9 -11
- package/bin/isojj.js +270 -102
- package/dist/index.cjs +2910 -1666
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2910 -1666
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
- package/src/api/repository.js +1099 -739
- package/src/backend/isomorphic-git-backend.js +183 -136
- package/src/backend/lazy-git-backend.js +12 -19
- package/src/browser/helpers.js +6 -2
- package/src/core/background-ops.js +58 -41
- package/src/core/bookmark-store.js +12 -8
- package/src/core/change-graph.js +26 -29
- package/src/core/conflict-model.js +66 -26
- package/src/core/jj-checkout.js +8 -4
- package/src/core/jj-operation-store.js +16 -12
- package/src/core/jj-tree-state.js +13 -9
- package/src/core/jj-view-store.js +11 -5
- package/src/core/merge-driver-registry.js +92 -46
- package/src/core/operation-log.js +30 -33
- package/src/core/revset-engine.js +1069 -485
- package/src/core/storage-manager.js +25 -19
- package/src/core/tag-store.js +2 -2
- package/src/core/user-config.js +3 -1
- package/src/core/working-copy.js +125 -9
- package/src/core/workspace-manager.js +2 -2
- package/src/drivers/built-in-drivers.js +66 -33
- package/src/types.d.ts +67 -1
- package/src/utils/errors.js +3 -3
- package/src/utils/id-generation.js +4 -4
- package/src/utils/validation.js +6 -10
package/bin/isojj.js
CHANGED
|
@@ -8,10 +8,33 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createJJ } from '../src/api/repository.js';
|
|
11
|
-
import
|
|
11
|
+
import nodeFs from 'fs';
|
|
12
|
+
import nodePath from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
// Flags that are always boolean and must never consume the next token as a
|
|
16
|
+
// value — otherwise `isojj describe --dryRun "fix typo"` would swallow the
|
|
17
|
+
// commit message as --dryRun's value instead of leaving it as a positional.
|
|
18
|
+
const BOOLEAN_FLAGS = new Set([
|
|
19
|
+
'json',
|
|
20
|
+
'dryRun',
|
|
21
|
+
'force',
|
|
22
|
+
'interactive',
|
|
23
|
+
'includeResolved',
|
|
24
|
+
'noTags',
|
|
25
|
+
'allowNew',
|
|
26
|
+
'preserveSnapshot',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/** Coerce a raw flag-value string to a real boolean when it looks like one. */
|
|
30
|
+
function coerceBoolean(value) {
|
|
31
|
+
if (value === 'true') return true;
|
|
32
|
+
if (value === 'false') return false;
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
12
35
|
|
|
13
36
|
// Parse command line arguments
|
|
14
|
-
function parseArgs(argv) {
|
|
37
|
+
export function parseArgs(argv) {
|
|
15
38
|
const args = argv.slice(2);
|
|
16
39
|
const command = args[0];
|
|
17
40
|
const opts = {};
|
|
@@ -21,24 +44,34 @@ function parseArgs(argv) {
|
|
|
21
44
|
|
|
22
45
|
if (arg.startsWith('--')) {
|
|
23
46
|
const key = arg.slice(2);
|
|
24
|
-
const nextArg = args[i + 1];
|
|
25
47
|
|
|
26
48
|
// Handle --flag and --key=value
|
|
27
49
|
if (arg.includes('=')) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
opts[
|
|
50
|
+
const eq = arg.indexOf('=');
|
|
51
|
+
const k = arg.slice(2, eq);
|
|
52
|
+
const v = arg.slice(eq + 1);
|
|
53
|
+
opts[k] = coerceBoolean(v);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
58
|
+
opts[key] = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const nextArg = args[i + 1];
|
|
63
|
+
if (nextArg !== undefined && !nextArg.startsWith('-')) {
|
|
64
|
+
opts[key] = coerceBoolean(nextArg);
|
|
32
65
|
i++;
|
|
33
66
|
} else {
|
|
34
67
|
opts[key] = true;
|
|
35
68
|
}
|
|
36
|
-
} else if (arg.startsWith('-')) {
|
|
69
|
+
} else if (arg.startsWith('-') && arg !== '-') {
|
|
37
70
|
// Handle short flags like -m "message"
|
|
38
71
|
const key = arg.slice(1);
|
|
39
72
|
const nextArg = args[i + 1];
|
|
40
73
|
|
|
41
|
-
if (nextArg && !nextArg.startsWith('-')) {
|
|
74
|
+
if (nextArg !== undefined && !nextArg.startsWith('-')) {
|
|
42
75
|
opts[key] = nextArg;
|
|
43
76
|
i++;
|
|
44
77
|
} else {
|
|
@@ -55,7 +88,7 @@ function parseArgs(argv) {
|
|
|
55
88
|
}
|
|
56
89
|
|
|
57
90
|
// Format output for different commands
|
|
58
|
-
function formatOutput(command, result) {
|
|
91
|
+
export function formatOutput(command, result, log = console.log) {
|
|
59
92
|
if (result === undefined || result === null) {
|
|
60
93
|
return;
|
|
61
94
|
}
|
|
@@ -63,16 +96,16 @@ function formatOutput(command, result) {
|
|
|
63
96
|
// Handle special output formatting for common commands
|
|
64
97
|
if (command === 'log') {
|
|
65
98
|
if (Array.isArray(result)) {
|
|
66
|
-
|
|
99
|
+
log('\nChange History:\n');
|
|
67
100
|
result.forEach((change, idx) => {
|
|
68
101
|
const shortId = change.changeId.slice(0, 12);
|
|
69
102
|
const desc = change.description || '(no description)';
|
|
70
103
|
const author = change.author?.name || 'unknown';
|
|
71
104
|
const timestamp = change.timestamp ? new Date(change.timestamp).toLocaleString() : '';
|
|
72
105
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (idx < result.length - 1)
|
|
106
|
+
log(` ${shortId} ${desc}`);
|
|
107
|
+
log(` ${author} ${timestamp}`);
|
|
108
|
+
if (idx < result.length - 1) log('');
|
|
76
109
|
});
|
|
77
110
|
return;
|
|
78
111
|
}
|
|
@@ -80,29 +113,31 @@ function formatOutput(command, result) {
|
|
|
80
113
|
|
|
81
114
|
if (command === 'status') {
|
|
82
115
|
if (result.workingCopy) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
log(`Working copy change: ${result.workingCopy.changeId.slice(0, 12)}`);
|
|
117
|
+
log(
|
|
118
|
+
`Parent change(s): ${(result.workingCopy.parents || []).map((p) => p.slice(0, 12)).join(', ')}`
|
|
119
|
+
);
|
|
120
|
+
log('');
|
|
86
121
|
}
|
|
87
122
|
|
|
88
123
|
if (result.modified?.length > 0) {
|
|
89
|
-
|
|
90
|
-
result.modified.forEach(file =>
|
|
124
|
+
log('Modified files:');
|
|
125
|
+
result.modified.forEach((file) => log(` M ${file}`));
|
|
91
126
|
}
|
|
92
127
|
|
|
93
128
|
if (result.added?.length > 0) {
|
|
94
|
-
|
|
95
|
-
result.added.forEach(file =>
|
|
129
|
+
log('Added files:');
|
|
130
|
+
result.added.forEach((file) => log(` A ${file}`));
|
|
96
131
|
}
|
|
97
132
|
|
|
98
133
|
if (result.removed?.length > 0) {
|
|
99
|
-
|
|
100
|
-
result.removed.forEach(file =>
|
|
134
|
+
log('Removed files:');
|
|
135
|
+
result.removed.forEach((file) => log(` D ${file}`));
|
|
101
136
|
}
|
|
102
137
|
|
|
103
138
|
if (result.conflicts?.length > 0) {
|
|
104
|
-
|
|
105
|
-
result.conflicts.forEach(file =>
|
|
139
|
+
log('Conflicted files:');
|
|
140
|
+
result.conflicts.forEach((file) => log(` C ${file}`));
|
|
106
141
|
}
|
|
107
142
|
|
|
108
143
|
return;
|
|
@@ -110,82 +145,199 @@ function formatOutput(command, result) {
|
|
|
110
145
|
|
|
111
146
|
if (command === 'diff') {
|
|
112
147
|
if (typeof result === 'string') {
|
|
113
|
-
|
|
148
|
+
log(result);
|
|
114
149
|
return;
|
|
115
150
|
}
|
|
116
151
|
}
|
|
117
152
|
|
|
118
153
|
// Default: pretty-print JSON
|
|
119
|
-
|
|
154
|
+
log(JSON.stringify(result, null, 2));
|
|
120
155
|
}
|
|
121
156
|
|
|
122
|
-
|
|
123
|
-
async function main() {
|
|
124
|
-
try {
|
|
125
|
-
const { command, opts } = parseArgs(process.argv);
|
|
126
|
-
|
|
127
|
-
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
128
|
-
console.log(`
|
|
157
|
+
export const HELP_TEXT = `
|
|
129
158
|
isojj - CLI for isomorphic-jj
|
|
130
159
|
|
|
131
160
|
Usage: isojj <command> [options]
|
|
132
161
|
|
|
133
|
-
|
|
134
|
-
init
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
describe -m <message>
|
|
138
|
-
new -m <message>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
162
|
+
Change management:
|
|
163
|
+
init Initialize a new repository
|
|
164
|
+
status Show working copy status
|
|
165
|
+
log [<revset> | --revset <e>] Show change history
|
|
166
|
+
describe -m <message> Set change description
|
|
167
|
+
new -m <message> Create a new change
|
|
168
|
+
commit -m <message> Describe + start a new change
|
|
169
|
+
diff [--revision <id>] Show changes
|
|
170
|
+
abandon [--revision <id>] Abandon a change
|
|
171
|
+
edit --revision <id> Edit an existing change
|
|
172
|
+
|
|
173
|
+
History editing:
|
|
174
|
+
squash / split / rebase / duplicate / parallelize
|
|
175
|
+
absorb [--dryRun] Absorb working-copy changes into ancestors
|
|
176
|
+
revert --revision <id> Create a change that undoes another (was: backout)
|
|
177
|
+
undo / redo Undo or redo the last operation
|
|
178
|
+
|
|
179
|
+
Files:
|
|
180
|
+
file.list [--revision <id>] List tracked files
|
|
181
|
+
file.show <path> Show file contents
|
|
182
|
+
file.search --pattern <re> Search file contents (jj file search)
|
|
183
|
+
file.annotate --path <p> Blame-style annotation
|
|
184
|
+
|
|
185
|
+
Bookmarks & tags:
|
|
186
|
+
bookmark.set --name <n> --to <id>
|
|
187
|
+
bookmark.advance --name <n> --to <id> Move a bookmark forward only
|
|
142
188
|
bookmark.list
|
|
189
|
+
tag.set --name <n> --changeId <id> Create or move a tag
|
|
190
|
+
tag.list
|
|
143
191
|
|
|
144
|
-
|
|
192
|
+
Signing:
|
|
193
|
+
sign [--revision <id>] [--backend ssh --key <k>]
|
|
194
|
+
unsign [--revision <id>]
|
|
145
195
|
|
|
196
|
+
Bisect:
|
|
146
197
|
bisect.start --good <id> --bad <id>
|
|
147
|
-
bisect.good
|
|
148
|
-
bisect.bad
|
|
149
|
-
bisect.status
|
|
150
|
-
bisect.reset
|
|
198
|
+
bisect.good / bisect.bad / bisect.status / bisect.reset
|
|
151
199
|
|
|
152
200
|
Options:
|
|
153
|
-
--dir <path> Repository directory (default: current directory)
|
|
201
|
+
--dir <path> Repository directory (default: current directory).
|
|
202
|
+
Like git/jj, the actual repo root is found by
|
|
203
|
+
searching this directory and its parents for .jj.
|
|
154
204
|
--json Output raw JSON (skip formatting)
|
|
205
|
+
-m, --message <msg> Message / description
|
|
206
|
+
-r, --revision <id> Target revision
|
|
207
|
+
|
|
208
|
+
Any additional --flags are passed straight through to the API method, so every
|
|
209
|
+
isomorphic-jj method is reachable (e.g. isojj file.search --pattern TODO).
|
|
155
210
|
|
|
156
211
|
Examples:
|
|
157
212
|
isojj init
|
|
158
|
-
isojj log
|
|
213
|
+
isojj log 'author(alice)'
|
|
159
214
|
isojj describe -m "Fix bug"
|
|
160
|
-
isojj
|
|
161
|
-
isojj
|
|
162
|
-
isojj
|
|
163
|
-
|
|
164
|
-
|
|
215
|
+
isojj revert --revision abc123
|
|
216
|
+
isojj file.search --pattern "TODO"
|
|
217
|
+
isojj bookmark.advance --name main --to def456
|
|
218
|
+
isojj tag.set --name v1.0 --changeId abc123
|
|
219
|
+
`;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Find the repository root by searching `startDir` and its parents for a
|
|
223
|
+
* `.jj` entry, mirroring how `git`/`jj` locate a repo from any subdirectory.
|
|
224
|
+
*
|
|
225
|
+
* @param {string} startDir - Directory to start searching from
|
|
226
|
+
* @param {any} fs - Filesystem implementation (must expose fs.promises.stat)
|
|
227
|
+
* @returns {Promise<string|null>} The directory containing `.jj`, or null
|
|
228
|
+
*/
|
|
229
|
+
export async function findRepoRoot(startDir, fs) {
|
|
230
|
+
let current = nodePath.resolve(startDir);
|
|
231
|
+
|
|
232
|
+
while (true) {
|
|
233
|
+
try {
|
|
234
|
+
await fs.promises.stat(nodePath.join(current, '.jj'));
|
|
235
|
+
return current;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (error.code !== 'ENOENT') throw error;
|
|
165
238
|
}
|
|
166
239
|
|
|
167
|
-
|
|
168
|
-
|
|
240
|
+
const parent = nodePath.dirname(current);
|
|
241
|
+
if (parent === current) {
|
|
242
|
+
return null; // Reached the filesystem root without finding .jj
|
|
243
|
+
}
|
|
244
|
+
current = parent;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
169
247
|
|
|
170
|
-
|
|
171
|
-
|
|
248
|
+
/**
|
|
249
|
+
* Dynamically load isomorphic-git (an optional peer dependency) so the CLI
|
|
250
|
+
* can create real Git-backed repositories when it's installed, while still
|
|
251
|
+
* working (storage-only "mock" mode) when it isn't.
|
|
252
|
+
*
|
|
253
|
+
* @param {(specifier: string) => Promise<any>} [importer] - Override for the
|
|
254
|
+
* dynamic `import()` call (used by tests to simulate a missing package
|
|
255
|
+
* without needing real module-resolution mocking).
|
|
256
|
+
* @returns {Promise<{git: any, http: any}>} git/http instances, or undefined
|
|
257
|
+
* for both if isomorphic-git isn't installed.
|
|
258
|
+
*/
|
|
259
|
+
export async function loadGitBackend(importer = (specifier) => import(specifier)) {
|
|
260
|
+
try {
|
|
261
|
+
const [{ default: git }, { default: http }] = await Promise.all([
|
|
262
|
+
importer('isomorphic-git'),
|
|
263
|
+
importer('isomorphic-git/http/node'),
|
|
264
|
+
]);
|
|
265
|
+
return { git, http };
|
|
266
|
+
} catch {
|
|
267
|
+
return { git: undefined, http: undefined };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Run the CLI for a given argv, returning a process exit code instead of
|
|
273
|
+
* calling process.exit() directly, so this is testable in-process.
|
|
274
|
+
*
|
|
275
|
+
* @param {string[]} argv - Full argv (e.g. process.argv)
|
|
276
|
+
* @param {object} [io]
|
|
277
|
+
* @param {any} [io.fs] - Filesystem implementation (defaults to Node's fs)
|
|
278
|
+
* @param {string} [io.cwd] - Working directory (defaults to process.cwd())
|
|
279
|
+
* @param {(msg: string) => void} [io.stdout] - stdout writer
|
|
280
|
+
* @param {(msg: string) => void} [io.stderr] - stderr writer
|
|
281
|
+
* @param {() => Promise<{git: any, http: any}>} [io.loadGitBackend] - override
|
|
282
|
+
* for loading the optional isomorphic-git backend (used by tests)
|
|
283
|
+
* @returns {Promise<number>} Exit code
|
|
284
|
+
*/
|
|
285
|
+
export async function run(argv, io = {}) {
|
|
286
|
+
const fs = io.fs || nodeFs;
|
|
287
|
+
const cwd = io.cwd || process.cwd();
|
|
288
|
+
const stdout = io.stdout || console.log;
|
|
289
|
+
const stderr = io.stderr || console.error;
|
|
290
|
+
const resolveGitBackend = io.loadGitBackend || loadGitBackend;
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const { command, opts } = parseArgs(argv);
|
|
294
|
+
|
|
295
|
+
if (command === 'version' || command === '--version' || command === '-v') {
|
|
296
|
+
// Always read package.json via real Node fs — it's outside the
|
|
297
|
+
// repository, so the (possibly injected/mock) repo `fs` doesn't apply.
|
|
298
|
+
const pkg = JSON.parse(
|
|
299
|
+
nodeFs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
|
300
|
+
);
|
|
301
|
+
stdout(`isojj ${pkg.version}`);
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
306
|
+
stdout(HELP_TEXT);
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Resolve the repository directory. Like git/jj, search the given
|
|
311
|
+
// directory (or cwd) and its parents for the .jj marker so the CLI works
|
|
312
|
+
// from any subdirectory of a repo, not just its root.
|
|
313
|
+
const startDir = opts.dir || cwd;
|
|
314
|
+
let dir = startDir;
|
|
172
315
|
|
|
173
|
-
// If not initializing, load existing repository state
|
|
174
316
|
if (command !== 'init') {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
await jj.graph.load();
|
|
181
|
-
await jj.workingCopy.load();
|
|
182
|
-
await jj.bookmarks.load();
|
|
183
|
-
} catch (error) {
|
|
184
|
-
if (error.code === 'ENOENT') {
|
|
185
|
-
throw new Error(`Not a jj repository (or any parent up to mount point /)\nStopped at: ${dir}\nRun 'isojj init' to initialize a repository`);
|
|
186
|
-
}
|
|
187
|
-
throw error;
|
|
317
|
+
const root = await findRepoRoot(startDir, fs);
|
|
318
|
+
if (!root) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`Not a jj repository (or any parent up to mount point /)\nStopped at: ${startDir}\nRun 'isojj init' to initialize a repository`
|
|
321
|
+
);
|
|
188
322
|
}
|
|
323
|
+
dir = root;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Wire up the real isomorphic-git backend when it's installed (it's an
|
|
327
|
+
// optional peer dependency), so `isojj init` creates a real .git
|
|
328
|
+
// directory instead of silently falling back to the storage-only "mock"
|
|
329
|
+
// backend. Attempted for every command (not just init) so an already
|
|
330
|
+
// Git-backed repo is loaded consistently.
|
|
331
|
+
const { git, http } = await resolveGitBackend();
|
|
332
|
+
|
|
333
|
+
// Initialize repository instance
|
|
334
|
+
const jj = await createJJ({ fs, dir, git, http });
|
|
335
|
+
|
|
336
|
+
if (command !== 'init') {
|
|
337
|
+
// Load existing repository state
|
|
338
|
+
await jj.graph.load();
|
|
339
|
+
await jj.workingCopy.load();
|
|
340
|
+
await jj.bookmarks.load();
|
|
189
341
|
}
|
|
190
342
|
|
|
191
343
|
// Handle nested commands (e.g., bisect.start, bookmark.set)
|
|
@@ -193,8 +345,8 @@ Examples:
|
|
|
193
345
|
let method = jj;
|
|
194
346
|
|
|
195
347
|
for (const part of parts) {
|
|
196
|
-
method = method[part];
|
|
197
|
-
if (
|
|
348
|
+
method = method?.[part];
|
|
349
|
+
if (method === undefined || method === null) {
|
|
198
350
|
throw new Error(`Unknown command: ${command}`);
|
|
199
351
|
}
|
|
200
352
|
}
|
|
@@ -203,29 +355,36 @@ Examples:
|
|
|
203
355
|
throw new Error(`${command} is not a valid command`);
|
|
204
356
|
}
|
|
205
357
|
|
|
206
|
-
// Map
|
|
207
|
-
|
|
358
|
+
// Map CLI flags to API options. Every parsed flag is passed through so new
|
|
359
|
+
// commands work without special-casing; a few common shorthands are then
|
|
360
|
+
// normalized on top.
|
|
361
|
+
const { dir: _dir, json: _json, _positional, ...passthrough } = opts;
|
|
362
|
+
const apiOpts = { ...passthrough };
|
|
208
363
|
|
|
209
|
-
// Common mappings
|
|
364
|
+
// Common shorthand mappings
|
|
210
365
|
if (opts.m || opts.message) apiOpts.message = opts.m || opts.message;
|
|
211
|
-
if (opts.
|
|
212
|
-
if (opts.revset) apiOpts.revset = opts.revset;
|
|
213
|
-
if (opts.name) apiOpts.name = opts.name;
|
|
214
|
-
if (opts.path) apiOpts.path = opts.path;
|
|
215
|
-
if (opts.good) apiOpts.good = opts.good;
|
|
216
|
-
if (opts.bad) apiOpts.bad = opts.bad;
|
|
366
|
+
if (opts.r || opts.revision) apiOpts.revision = opts.r || opts.revision;
|
|
217
367
|
if (opts.dryRun) apiOpts.dryRun = true;
|
|
218
|
-
if (opts.parents) {
|
|
219
|
-
apiOpts.parents = opts.parents.includes(',')
|
|
220
|
-
? opts.parents.split(',')
|
|
221
|
-
: [opts.parents];
|
|
368
|
+
if (typeof opts.parents === 'string') {
|
|
369
|
+
apiOpts.parents = opts.parents.includes(',') ? opts.parents.split(',') : [opts.parents];
|
|
222
370
|
}
|
|
223
371
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
372
|
+
// Positional arguments: map the first positional to the most natural
|
|
373
|
+
// parameter for common file/query commands.
|
|
374
|
+
if (_positional?.length > 0) {
|
|
375
|
+
const first = _positional[0];
|
|
376
|
+
if (
|
|
377
|
+
(command === 'read' || command === 'remove' || command.startsWith('file.')) &&
|
|
378
|
+
!apiOpts.path
|
|
379
|
+
) {
|
|
380
|
+
apiOpts.path = first;
|
|
381
|
+
} else if (command === 'log' && !apiOpts.revset) {
|
|
382
|
+
apiOpts.revset = first;
|
|
383
|
+
} else if (
|
|
384
|
+
(command === 'describe' || command === 'new' || command === 'commit') &&
|
|
385
|
+
!apiOpts.message
|
|
386
|
+
) {
|
|
387
|
+
apiOpts.message = first;
|
|
229
388
|
}
|
|
230
389
|
}
|
|
231
390
|
|
|
@@ -234,29 +393,38 @@ Examples:
|
|
|
234
393
|
|
|
235
394
|
// Format and output result
|
|
236
395
|
if (opts.json) {
|
|
237
|
-
|
|
396
|
+
stdout(JSON.stringify(result, null, 2));
|
|
238
397
|
} else {
|
|
239
|
-
formatOutput(command, result);
|
|
398
|
+
formatOutput(command, result, stdout);
|
|
240
399
|
}
|
|
241
400
|
|
|
242
|
-
|
|
401
|
+
return 0;
|
|
243
402
|
} catch (error) {
|
|
244
|
-
|
|
403
|
+
stderr(`Error: ${error.message}`);
|
|
245
404
|
|
|
246
405
|
if (error.code) {
|
|
247
|
-
|
|
406
|
+
stderr(`Code: ${error.code}`);
|
|
248
407
|
}
|
|
249
408
|
|
|
250
409
|
if (error.context?.suggestion) {
|
|
251
|
-
|
|
410
|
+
stderr(`Suggestion: ${error.context.suggestion}`);
|
|
252
411
|
}
|
|
253
412
|
|
|
254
413
|
if (process.env.DEBUG) {
|
|
255
|
-
|
|
414
|
+
stderr(error.stack);
|
|
256
415
|
}
|
|
257
416
|
|
|
258
|
-
|
|
417
|
+
return 1;
|
|
259
418
|
}
|
|
260
419
|
}
|
|
261
420
|
|
|
262
|
-
|
|
421
|
+
// Only run (and exit the process) when this file is executed directly, not
|
|
422
|
+
// when it's imported by tests. Comparing `import.meta.url` (a file:// URL,
|
|
423
|
+
// e.g. "file:///C:/repo/bin/isojj.js" on Windows) against the raw
|
|
424
|
+
// `process.argv[1]` string (a native path, e.g. "C:\repo\bin\isojj.js") is
|
|
425
|
+
// never equal on Windows — convert the URL to a native path first.
|
|
426
|
+
const isMainModule =
|
|
427
|
+
!!process.argv[1] && fileURLToPath(import.meta.url) === nodePath.resolve(process.argv[1]);
|
|
428
|
+
if (isMainModule) {
|
|
429
|
+
run(process.argv).then((code) => process.exit(code));
|
|
430
|
+
}
|