datagrok-tools 6.7.9 → 6.7.10

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.
@@ -0,0 +1,458 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.src = src;
8
+ var _fs = _interopRequireDefault(require("fs"));
9
+ var _path = _interopRequireDefault(require("path"));
10
+ var _child_process = require("child_process");
11
+ var color = _interopRequireWildcard(require("../utils/color-utils"));
12
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
13
+ const arity = {
14
+ clone: [0, Infinity],
15
+ add: [1, Infinity],
16
+ sparse: [2, Infinity],
17
+ switch: [0, 0],
18
+ status: [0, 0],
19
+ pull: [0, 0],
20
+ at: [1, 1],
21
+ bump: [0, Infinity],
22
+ wt: [3, Infinity],
23
+ doctor: [0, 0]
24
+ };
25
+ const env = {
26
+ ...process.env,
27
+ ...(process.stdin.isTTY ? {} : {
28
+ GIT_TERMINAL_PROMPT: '0'
29
+ })
30
+ };
31
+ for (const key of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_PREFIX']) delete env[key];
32
+ let explain = false;
33
+ const quote = s => /^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
34
+ function git(dir, args) {
35
+ const r = (0, _child_process.spawnSync)('git', ['-C', dir, ...args], {
36
+ encoding: 'utf-8',
37
+ env,
38
+ maxBuffer: 64 * 1024 * 1024
39
+ });
40
+ return {
41
+ ok: r.status === 0,
42
+ fatal: r.status === 128,
43
+ out: (r.stdout ?? '').trim(),
44
+ err: (r.stderr ?? '').trim()
45
+ };
46
+ }
47
+ function step(dir, args) {
48
+ const rel = _path.default.relative(process.cwd(), dir);
49
+ console.log(`$ git ${rel ? `-C ${quote(rel)} ` : ''}${args.map(quote).join(' ')}`);
50
+ return explain || (0, _child_process.spawnSync)('git', ['-C', dir, ...args], {
51
+ stdio: 'inherit',
52
+ env
53
+ }).status === 0;
54
+ }
55
+ function findRoot() {
56
+ let dir = git(process.cwd(), ['rev-parse', '--show-toplevel']);
57
+ if (!dir.ok) throw new Error(dir.err);
58
+ for (let up; (up = git(dir.out, ['rev-parse', '--show-superproject-working-tree'])).out;) dir = up;
59
+ return git(dir.out, ['ls-files', '--', '.gitmodules']).out ? dir.out : undefined;
60
+ }
61
+ function children(dir) {
62
+ if (!git(dir, ['ls-files', '--', '.gitmodules']).out) return [];
63
+ const file = _fs.default.existsSync(_path.default.join(dir, '.gitmodules')) ? ['-f', '.gitmodules'] : ['--blob', ':./.gitmodules'];
64
+ const r = git(dir, ['config', ...file, '--get-regexp', '^submodule\\..*\\.(path|url|branch)$']);
65
+ if (r.fatal) throw new Error(`${_path.default.join(dir, '.gitmodules')}: ${r.err}`);
66
+ const mods = new Map();
67
+ for (const line of r.out.split('\n')) {
68
+ const m = /^submodule\.(.+)\.(path|url|branch) (.*)$/.exec(line);
69
+ if (m && !m[3].startsWith('-')) mods.set(m[1], {
70
+ ...mods.get(m[1]),
71
+ [m[2]]: m[3]
72
+ });
73
+ }
74
+ const links = new Set(git(dir, ['ls-files', '-z', '--stage']).out.split('\0').filter(l => l.startsWith('160000 ')).map(l => l.slice(l.indexOf('\t') + 1)));
75
+ return [...mods].filter(([, c]) => links.has(c.path) && c.url).map(([name, c]) => ({
76
+ name,
77
+ path: c.path,
78
+ url: c.url,
79
+ branch: c.branch,
80
+ dir: _path.default.join(dir, c.path)
81
+ }));
82
+ }
83
+ const cloned = dir => _fs.default.existsSync(_path.default.join(dir, '.git')) && (_fs.default.statSync(_path.default.join(dir, '.git')).isFile() || !git(dir, ['rev-parse', '--show-prefix']).out);
84
+ const empty = dir => !_fs.default.existsSync(dir) || !_fs.default.readdirSync(dir).length;
85
+ function* repos(dir, c, owner = dir) {
86
+ yield {
87
+ dir,
88
+ branch: c?.branch,
89
+ c,
90
+ owner
91
+ };
92
+ for (const k of children(dir).filter(x => cloned(x.dir))) yield* repos(k.dir, k, dir);
93
+ }
94
+ function state(dir) {
95
+ const r = git(dir, ['status', '--porcelain=v2', '--branch', '--untracked-files=no', '--ignore-submodules=all']);
96
+ const head = /^# branch\.head (.*)$/m.exec(r.out)?.[1] ?? '';
97
+ return {
98
+ error: r.ok ? '' : r.err.split('\n')[0],
99
+ branch: head === '(detached)' ? '' : head,
100
+ upstream: /^# branch\.ab /m.test(r.out),
101
+ ahead: Number(/^# branch\.ab \+(\d+)/m.exec(r.out)?.[1] ?? 0),
102
+ changed: r.out.split('\n').filter(l => l && !l.startsWith('#')).length
103
+ };
104
+ }
105
+ const orphaned = dir => !!git(dir, ['rev-list', '-n1', 'HEAD', '--not', '--branches', '--remotes', '--tags']).out;
106
+ const label = (root, dir) => _path.default.relative(root, dir) || '(umbrella)';
107
+ const gitC = dir => `git -C ${quote(_path.default.relative(process.cwd(), dir) || '.')}`;
108
+ function locate(root, rel) {
109
+ const chain = [];
110
+ for (let dir = root, rest = rel.replace(/\/+$/, '');;) {
111
+ const c = children(dir).find(x => rest === x.path || rest.startsWith(x.path + '/'));
112
+ if (!c) return undefined;
113
+ chain.push(c);
114
+ if (rest === c.path) return chain;
115
+ [dir, rest] = [c.dir, rest.slice(c.path.length + 1)];
116
+ }
117
+ }
118
+ const modules = (owner, c) => _path.default.resolve(owner, git(owner, ['rev-parse', '--git-path', `modules/${c.name}`]).out);
119
+ function rollback(owner, c, made) {
120
+ if (cloned(c.dir) || empty(c.dir)) step(owner, ['submodule', 'deinit', '--force', '--', c.path]);
121
+ step(owner, ['config', `submodule.${c.name}.active`, 'false']);
122
+ if (made) _fs.default.rmSync(made, {
123
+ recursive: true,
124
+ force: true
125
+ });
126
+ }
127
+ function cloneChild(root, owner, c, failed) {
128
+ if (cloned(c.dir)) return true;
129
+ const gitdir = modules(owner, c);
130
+ const made = _fs.default.existsSync(gitdir) ? undefined : gitdir;
131
+ if (!step(owner, ['submodule', 'update', '--init', '--require-init', '--checkout', '--filter=blob:none', '--', c.path])) {
132
+ rollback(owner, c, made);
133
+ failed.push(label(root, c.dir));
134
+ return false;
135
+ }
136
+ if (c.branch && !step(c.dir, ['switch', c.branch])) failed.push(label(root, c.dir));
137
+ if (_fs.default.existsSync(_path.default.join(c.dir, '.gitmodules'))) step(c.dir, ['config', 'push.recurseSubmodules', 'check']);
138
+ return true;
139
+ }
140
+ function cloneTree(root, owner, c, failed) {
141
+ const fresh = !cloned(c.dir);
142
+ if (!cloneChild(root, owner, c, failed) || !git(c.dir, ['ls-files', '--', '.gitmodules']).out) return;
143
+ const cur = fresh ? c.branch : git(c.dir, ['branch', '--show-current']).out;
144
+ if (cur === c.branch || !cur && git(c.dir, ['rev-parse', 'HEAD']).out === git(owner, ['rev-parse', `:${c.path}`]).out) children(c.dir).forEach(k => cloneTree(root, c.dir, k, failed));
145
+ }
146
+ function addSpec(root, dir, spec, failed) {
147
+ const kids = children(dir);
148
+ const parent = kids.find(c => spec.startsWith(c.path + '/'));
149
+ if (parent) {
150
+ return !cloneChild(root, dir, parent, failed) || explain && !cloned(parent.dir) || addSpec(root, parent.dir, spec.slice(parent.path.length + 1), failed);
151
+ }
152
+ const listed = git(dir, ['ls-files', '-z', '--', spec]).out.split('\0');
153
+ const targets = kids.filter(c => listed.includes(c.path));
154
+ targets.forEach(c => cloneTree(root, dir, c, failed));
155
+ return targets.length > 0;
156
+ }
157
+ function add(root, sel) {
158
+ const specs = (sel.length ? sel : ['default']).flatMap(s => {
159
+ const profile = git(root, ['config', '-f', '.gitprofiles', '--get-all', `profile.${s}.path`]);
160
+ if (profile.fatal) throw new Error(`${_path.default.join(root, '.gitprofiles')}: ${profile.err}`);
161
+ return profile.ok ? profile.out.split('\n') : sel.length ? [s] : ['.'];
162
+ });
163
+ const failed = [];
164
+ const unknown = specs.map(s => s.replace(/^\.\//, '').replace(/\/+$/, '')).filter(s => !addSpec(root, root, s, failed));
165
+ unknown.forEach(s => color.error(`${s}: no profile or submodule by that name`));
166
+ if (failed.length) color.error(`Failed (see the git errors above): ${failed.join(', ')}`);
167
+ return !unknown.length && !failed.length;
168
+ }
169
+ function clone(sel, url, dir) {
170
+ if (!step(process.cwd(), ['clone', '-c', 'push.recurseSubmodules=check', '--filter=blob:none', '--', url, dir])) return false;
171
+ if (!explain) return add(dir, sel);
172
+ console.log(`# then, in ${dir}: grok src add ${sel.join(' ') || '(profile "default", else every child)'}`);
173
+ return true;
174
+ }
175
+ function sparse(root, rel, globs) {
176
+ const chain = locate(root, rel);
177
+ if (!chain) return color.error(`${rel}: not a submodule, or its parent is not cloned`), false;
178
+ const c = chain[chain.length - 1];
179
+ const owner = chain.length > 1 ? chain[chain.length - 2].dir : root;
180
+ const absorb = ['submodule', 'absorbgitdirs', '--', c.path];
181
+ const set = ['sparse-checkout', 'set', '--no-cone', ...globs];
182
+ if (cloned(c.dir)) return step(owner, absorb) && step(c.dir, set);
183
+ const gitdir = modules(owner, c);
184
+ if (_fs.default.existsSync(gitdir)) return color.error(`${gitdir} exists from an earlier clone: grok src add ${rel}, then sparse again`), false;
185
+ const ok = step(owner, ['submodule', 'init', '--', c.path]) && step(owner, ['clone', '--filter=blob:none', '--no-checkout', '--', git(owner, ['config', `submodule.${c.name}.url`]).out || '<url>', c.path]) && step(owner, absorb) && step(c.dir, set) && step(c.dir, ['checkout', c.branch ?? git(owner, ['rev-parse', `:${c.path}`]).out, '--']);
186
+ if (!ok) rollback(owner, c, gitdir);
187
+ return ok;
188
+ }
189
+ function switchAll(root) {
190
+ let ok = true;
191
+ for (const {
192
+ dir,
193
+ branch
194
+ } of repos(root)) {
195
+ const s = state(dir);
196
+ if (!branch || s.branch === branch) continue;
197
+ if (s.error || s.changed || !s.branch && orphaned(dir)) {
198
+ color.warn(`${label(root, dir)}: ${s.error || 'uncommitted changes or commits on no branch'}, not switched to ${branch}`);
199
+ ok = false;
200
+ } else ok = step(dir, ['switch', branch]) && ok;
201
+ }
202
+ return ok;
203
+ }
204
+ function status(root) {
205
+ const head = ['REPO', 'BRANCH', 'UNPUSHED', 'CHANGED'];
206
+ const rows = [...repos(root)].map(({
207
+ dir
208
+ }) => {
209
+ const s = state(dir);
210
+ const unpushed = !s.branch ? '' : !s.upstream ? 'no upstream' : s.ahead ? `${s.ahead}` : '';
211
+ return [label(root, dir), s.error ? '(error: grok src doctor)' : s.branch || '(detached)', unpushed, s.changed ? `${s.changed}` : ''];
212
+ });
213
+ const widths = head.map((_, i) => Math.max(...[head, ...rows].map(r => r[i].length)));
214
+ for (const r of [head, ...rows]) console.log(r.map((x, i) => x.padEnd(widths[i])).join(' ').trimEnd());
215
+ return !rows.some(r => r[1].startsWith('(error: '));
216
+ }
217
+ function pull(root) {
218
+ const skipped = [];
219
+ const failed = [];
220
+ for (const {
221
+ dir,
222
+ branch
223
+ } of repos(root)) {
224
+ const s = state(dir);
225
+ if (s.error) failed.push(label(root, dir));else if (!s.branch || !s.upstream) {
226
+ if (branch || dir === root) skipped.push(label(root, dir));
227
+ } else if (!step(dir, ['pull', '--ff-only', '--no-recurse-submodules'])) failed.push(label(root, dir));
228
+ }
229
+ if (skipped.length) color.warn(`Skipped (detached or no upstream): ${skipped.join(', ')}`);
230
+ if (failed.length) color.error(`Not pulled: ${failed.join(', ')}`);
231
+ return !failed.length;
232
+ }
233
+ function at(root, ref) {
234
+ const all = [...repos(root)];
235
+ const loose = all.flatMap(({
236
+ c,
237
+ owner
238
+ }) => c && !git(owner, ['config', `submodule.${c.name}.url`]).ok ? [`${gitC(owner)} submodule init -- ${quote(c.path)}`] : []);
239
+ if (loose.length) return color.error(`Cloned but not initialized, so git would leave them off the pins: ${loose.join('; ')}`), false;
240
+ const blocked = all.filter(({
241
+ dir
242
+ }) => {
243
+ const s = state(dir);
244
+ return s.error || s.changed || !s.branch && orphaned(dir) || !git(dir, ['diff', '--cached', '--quiet', '--ignore-submodules=none']).ok;
245
+ }).map(({
246
+ dir
247
+ }) => label(root, dir));
248
+ if (blocked.length) return color.error(`Uncommitted changes or commits on no branch in ${blocked.join(', ')}: commit, stash or branch first`), false;
249
+ return step(root, ['checkout', ref, '--']) && step(root, ['submodule', 'update', '--recursive', '--checkout']);
250
+ }
251
+ function bump(root, rels) {
252
+ const failed = [];
253
+ const owners = new Map();
254
+ let checked = 0;
255
+ const skipped = [];
256
+ const visit = (c, owner) => {
257
+ let o = owners.get(owner);
258
+ if (!o) {
259
+ const staged = !git(owner, ['diff', '--cached', '--quiet', '--ignore-submodules=none']).ok;
260
+ const detached = !state(owner).branch;
261
+ if (staged || detached && rels.length) failed.push(`${label(root, owner)} (${staged ? 'staged changes' : 'not on a branch'})`);else if (detached) skipped.push(label(root, owner));
262
+ owners.set(owner, o = {
263
+ ok: !staged && !detached,
264
+ links: []
265
+ });
266
+ }
267
+ const name = label(root, c.dir);
268
+ const branch = c.branch === '.' ? state(owner).branch : c.branch;
269
+ if (!o.ok || !branch) {
270
+ if (o.ok && rels.length) failed.push(`${name} (no branch in .gitmodules)`);
271
+ return;
272
+ }
273
+ if (git(c.dir, ['config', 'remote.origin.url']).out !== git(owner, ['config', `submodule.${c.name}.url`]).out) return failed.push(`${name} (origin differs from .gitmodules: grok src doctor --fix)`);
274
+ checked++;
275
+ const ref = `refs/remotes/origin/${branch}`;
276
+ if (!step(c.dir, ['fetch', '--no-recurse-submodules', 'origin', `+refs/heads/${branch}:${ref}`])) return failed.push(name);
277
+ const tip = explain ? git(c.dir, ['ls-remote', 'origin', `refs/heads/${branch}`]).out.split('\t')[0] : git(c.dir, ['rev-parse', '--verify', '-q', `${ref}^{commit}`]).out;
278
+ const pin = git(owner, ['rev-parse', `HEAD:${c.path}`]).out;
279
+ if (!tip) return failed.push(`${name} (origin has no ${branch})`);
280
+ if (tip !== pin && !explain && !git(c.dir, ['merge-base', '--is-ancestor', pin, tip]).ok) return failed.push(`${name} (the pin is not on origin/${branch}; move it by hand)`);
281
+ const s = state(c.dir);
282
+ const head = git(c.dir, ['rev-parse', 'HEAD']).out;
283
+ const moved = head === tip || !s.changed && (s.branch === branch ? step(c.dir, ['merge', '--ff-only', ref]) : !s.branch && git(c.dir, ['merge-base', '--is-ancestor', head, tip]).ok && step(c.dir, ['checkout', '--detach', tip]));
284
+ if (tip === pin) return;
285
+ if (!moved) color.warn(`${name}: checkout left as it is (local changes, or not on ${branch} or the pin); pinned to origin/${branch}`);
286
+ o.links.push({
287
+ path: c.path,
288
+ tip
289
+ });
290
+ };
291
+ const wanted = [...new Set(rels.map(r => r.replace(/\\/g, '/').replace(/^(\.\/)+/, '').replace(/\/+$/, '')))];
292
+ for (const rel of wanted.sort((a, b) => a.split('/').length - b.split('/').length)) {
293
+ const chain = locate(root, rel);
294
+ const c = chain?.[chain.length - 1];
295
+ if (!chain || !c || !cloned(c.dir)) failed.push(chain ? `${rel} (not cloned: grok src add ${rel})` : `${rel} (not a submodule)`);else visit(c, chain.length > 1 ? chain[chain.length - 2].dir : root);
296
+ }
297
+ if (!rels.length) {
298
+ for (const {
299
+ c,
300
+ owner
301
+ } of repos(root)) {
302
+ if (c) visit(c, owner);
303
+ }
304
+ }
305
+ let bumped = false;
306
+ for (const [owner, {
307
+ links
308
+ }] of owners) {
309
+ if (!links.length) continue;
310
+ bumped = true;
311
+ const paths = links.map(l => l.path);
312
+ if (step(owner, ['update-index', ...links.flatMap(l => ['--cacheinfo', `160000,${l.tip},${l.path}`])]) && step(owner, ['commit', '-m', `Bump ${paths.join(', ')}`])) {
313
+ if (owner !== root) console.log(`${label(root, owner)}: push it, then grok src bump ${_path.default.relative(root, owner).split(_path.default.sep).join('/')}`);
314
+ } else {
315
+ if (!explain) git(owner, ['reset', '-q', '--', ...paths]);
316
+ failed.push(label(root, owner));
317
+ }
318
+ }
319
+ if (skipped.length) color.warn(`Not on a branch, so their children were not bumped: ${skipped.join(', ')}`);else if (!bumped && !failed.length) console.log(checked ? 'Every pin is at the tip of its branch.' : 'No child with a branch to bump.');
320
+ if (failed.length) color.error(`Not bumped: ${failed.join(', ')}`);
321
+ return !failed.length;
322
+ }
323
+ function wtAdd(root, name, rels, destArg) {
324
+ const dest = _path.default.resolve(destArg ?? `${root}-worktrees/${name}`);
325
+ const dirs = new Set([root]);
326
+ for (const rel of rels) {
327
+ const chain = locate(root, rel);
328
+ if (!chain || !cloned(chain[chain.length - 1].dir)) return color.error(`${rel}: not a cloned submodule`), false;
329
+ chain.forEach(c => dirs.add(c.dir));
330
+ }
331
+ const todo = [...dirs].map(dir => ({
332
+ dir,
333
+ to: _path.default.join(dest, _path.default.relative(root, dir))
334
+ }));
335
+ for (const {
336
+ dir,
337
+ to
338
+ } of todo) {
339
+ const elsewhere = git(dir, ['for-each-ref', '--format=%(worktreepath)', `refs/heads/${name}`]).out;
340
+ if (cloned(to) ? git(to, ['branch', '--show-current']).out !== name : elsewhere) {
341
+ const where = elsewhere && (git(dir, [`--git-dir=${elsewhere}`, 'rev-parse', '--show-toplevel']).out || elsewhere);
342
+ return color.error(cloned(to) ? `${to} exists and is not on ${name}` : `${label(root, dir)}: ${name} is checked out in ${where} (if that is gone: ${gitC(dir)} worktree prune)`), false;
343
+ }
344
+ }
345
+ for (const {
346
+ dir,
347
+ to
348
+ } of todo.filter(t => !cloned(t.to))) {
349
+ const local = git(dir, ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).ok;
350
+ const remote = !local && git(dir, ['show-ref', '--verify', '--quiet', `refs/remotes/origin/${name}`]).ok;
351
+ const start = local ? ['--', to, name] : remote ? ['--track', '-b', name, '--', to, `origin/${name}`] : ['-b', name, '--', to];
352
+ if (!step(dir, ['worktree', 'add', ...start])) return false;
353
+ const own = explain ? '<worktree gitdir>/config.worktree' : git(to, ['--work-tree=.', 'rev-parse', '--path-format=absolute', '--git-path', 'config.worktree']).out;
354
+ if (dir !== root && !step(dir, ['config', '-f', own, 'core.worktree', to])) return false;
355
+ }
356
+ console.log(`Worktrees: ${dest}`);
357
+ return true;
358
+ }
359
+ function doctor(root, fix) {
360
+ const problems = [];
361
+ const synced = [];
362
+ if (process.platform === 'win32' && git(root, ['config', '--type=bool', 'core.longpaths']).out !== 'true') problems.push('core.longpaths is not set: git config --global core.longpaths true');
363
+ for (const {
364
+ dir,
365
+ branch,
366
+ c,
367
+ owner
368
+ } of repos(root)) {
369
+ const s = state(dir);
370
+ const name = label(root, dir);
371
+ const at = gitC(dir);
372
+ if (s.error) {
373
+ problems.push(`${name}: ${s.error}`);
374
+ continue;
375
+ }
376
+ const orphan = !s.branch && orphaned(dir);
377
+ if (!s.branch && (branch || orphan)) problems.push(`${name}: detached HEAD: ${orphan ? `${at} switch -c <name>` : 'grok src switch'}`);
378
+ if (s.ahead) problems.push(`${name}: ${s.ahead} unpushed commit(s): ${at} push`);
379
+ const local = s.branch && !s.upstream ? Number(git(dir, ['rev-list', '--count', 'HEAD', '--not', '--remotes']).out) : 0;
380
+ if (local) problems.push(`${name}: ${local} commit(s) on no remote: ${at} push -u origin ${s.branch}`);
381
+ if (!c) continue;
382
+ const base = git(owner, ['config', 'remote.origin.url']).out.replace(/\/+$/, '');
383
+ const url = c.url.startsWith('../') ? base && base.replace(/[^/:]+$/, '') + c.url.slice(3) : c.url;
384
+ if (!url) {
385
+ problems.push(`${name}: ${label(root, owner)} has no origin remote`);
386
+ continue;
387
+ }
388
+ const known = git(owner, ['config', `submodule.${c.name}.url`]);
389
+ if (known.out !== url || git(dir, ['config', 'remote.origin.url']).out !== url) {
390
+ problems.push(`${name}: URL differs from .gitmodules (${url}): grok src doctor --fix`);
391
+ synced.push(!fix || (known.ok || step(owner, ['submodule', 'init', '--', c.path])) && step(owner, ['config', `submodule.${c.name}.url`, url]) && step(dir, ['remote', 'set-url', 'origin', url]));
392
+ }
393
+ }
394
+ if (fix && !explain && synced.length && synced.every(Boolean)) return doctor(root, false);
395
+ problems.forEach(p => console.log(p));
396
+ if (!problems.length) color.success('No problems found.');
397
+ return !problems.length || fix && explain && synced.length > 0;
398
+ }
399
+ function run(cmd, rest, argv) {
400
+ if (cmd === 'clone') {
401
+ const url = argv.url ?? 'git@github.com:datagrok-ai/datagrok.git';
402
+ return clone(rest, url, _path.default.resolve(argv.dir ?? _path.default.basename(url).replace(/\.git$/, '')));
403
+ }
404
+ const root = findRoot();
405
+ if (!root) return color.error('Not inside an umbrella checkout (a repo with .gitmodules)'), false;
406
+ const [gitDir, commonDir] = git(root, ['rev-parse', '--path-format=absolute', '--git-dir', '--git-common-dir']).out.split('\n');
407
+ if (gitDir !== commonDir && (['add', 'sparse', 'switch', 'at'].includes(cmd) || cmd === 'doctor' && argv.fix)) return color.error(`grok src ${cmd} changes what the main checkout shares (config, branches): run it in the main checkout`), false;
408
+ switch (cmd) {
409
+ case 'add':
410
+ return add(root, rest);
411
+ case 'sparse':
412
+ return sparse(root, rest[0], rest.slice(1));
413
+ case 'switch':
414
+ return switchAll(root);
415
+ case 'status':
416
+ return status(root);
417
+ case 'pull':
418
+ return pull(root);
419
+ case 'at':
420
+ return at(root, rest[0]);
421
+ case 'bump':
422
+ return bump(root, rest);
423
+ case 'wt':
424
+ return wtAdd(root, rest[1], rest.slice(2), argv.dir);
425
+ default:
426
+ return doctor(root, !!argv.fix);
427
+ }
428
+ }
429
+ async function src(argv) {
430
+ const [cmd, ...rest] = argv._.slice(1);
431
+ const options = Object.keys(argv).filter(k => !['_', 'explain', 'fix', 'url', 'dir'].includes(k));
432
+ if (options.length || [argv.url, argv.dir].some(v => v !== undefined && (typeof v !== 'string' || !v))) {
433
+ color.error(options.length ? `Unknown option --${options[0]}` : '--url and --dir take one value');
434
+ return false;
435
+ }
436
+ if (!cmd) {
437
+ console.log((await Promise.resolve().then(() => _interopRequireWildcard(require('./help')))).help.src);
438
+ return true;
439
+ }
440
+ if (!Object.hasOwn(arity, cmd) || rest.length < arity[cmd][0] || rest.length > arity[cmd][1] || cmd === 'wt' && rest[0] !== 'add') {
441
+ color.error(`Unknown command or wrong arguments: grok src ${cmd}`);
442
+ return false;
443
+ }
444
+ const [major, minor] = (/(\d+)\.(\d+)/.exec(git('.', ['--version']).out) ?? []).slice(1).map(Number);
445
+ if (!(major * 100 + minor >= 236)) {
446
+ color.error('grok src needs git 2.36 or newer');
447
+ process.exitCode = 1;
448
+ return true;
449
+ }
450
+ explain = !!argv.explain;
451
+ try {
452
+ if (!run(cmd, rest, argv)) process.exitCode = 1;
453
+ } catch (e) {
454
+ color.error(e instanceof Error ? e.message : String(e));
455
+ process.exitCode = 1;
456
+ }
457
+ return true;
458
+ }
package/bin/grok.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  // minimist sets every boolean option to false when it is absent, and commands such as `grok api`
3
- // reject any key they do not expect — so each boolean reaches only the commands that read it.
4
- const commandBooleans = {dartium: ['test', 'stresstest'], 'send-email': ['server', 's']};
3
+ // reject any key they do not expect — so an absent boolean reaches only the commands that read it.
4
+ const commandBooleans = {dartium: ['test', 'stresstest'], 'send-email': ['server', 's'],
5
+ 'read-only': ['connect-folder'], foreground: ['connect-folder'], force: ['connect-folder'], follow: ['connect-folder'],
6
+ explain: ['src'], fix: ['src']};
5
7
  const argv = require('minimist')(process.argv.slice(2), {
6
8
  alias: {k: 'key', h: 'help', r: 'recursive'},
7
9
  boolean: Object.keys(commandBooleans),
8
- // keep versions verbatim — minimist would coerce '1.10' to the number 1.1
9
- string: ['version', 'prefix'],
10
+ // keep these verbatim — minimist would coerce '1.10' to the number 1.1
11
+ string: ['version', 'prefix', 'url', 'dir', '_'],
10
12
  });
11
13
  // minimist maps `--no-retry` to `{retry: false}`, so the `args['no-retry']` checks in
12
14
  // test.ts / playwright-runner.ts never fired and `--no-retry` was silently ignored
@@ -43,6 +45,8 @@ const commands = {
43
45
  server: lazy('server', 'server'),
44
46
  s: lazy('server', 'server'),
45
47
  setup: lazy('setup', 'setup'),
48
+ src: lazy('src', 'src'),
49
+ 'connect-folder': lazy('connect-folder', 'connectFolder'),
46
50
  };
47
51
 
48
52
  // `--version` is a string option (grok publish --version 1.10), so a bare `grok --version` parses as ''.
@@ -63,7 +67,7 @@ function printUsage(command) {
63
67
 
64
68
  const command = argv['_'][0];
65
69
  for (const [flag, owners] of Object.entries(commandBooleans))
66
- if (!owners.includes(command))
70
+ if (!owners.includes(command) && argv[flag] === false)
67
71
  delete argv[flag];
68
72
  if (command in commands) {
69
73
  try {