datagrok-tools 6.7.8 → 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.
- package/.devcontainer/Dockerfile.pkg_dev +1 -1
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +33 -0
- package/CLAUDE.md +13 -0
- package/GROK_S.md +138 -6
- package/README.md +14 -0
- package/bin/commands/add.js +4 -4
- package/bin/commands/api.js +49 -20
- package/bin/commands/check.js +43 -1
- package/bin/commands/connect-folder.js +411 -0
- package/bin/commands/help.js +68 -2
- package/bin/commands/publish.js +6 -0
- package/bin/commands/server-domains.js +24 -7
- package/bin/commands/server-observe.js +854 -0
- package/bin/commands/server.js +72 -5
- package/bin/commands/src.js +458 -0
- package/bin/grok.js +9 -5
- package/bin/utils/connect-folder/docker.js +244 -0
- package/bin/utils/connect-folder/docker.ts +259 -0
- package/bin/utils/ent-helpers.js +2 -2
- package/bin/utils/migrate/walker.js +2 -1
- package/bin/utils/migrate/walker.ts +1 -1
- package/bin/utils/node-dapi.js +56 -17
- package/bin/utils/server-client.js +8 -1
- package/bin/utils/test-utils.js +54 -2
- package/domain-schema.schema.json +50 -1
- package/entity-template/domain-app-imports.js +2 -2
- package/package.json +5 -2
- package/plugins/func-gen-plugin.js +1 -1
- package/vitest.config.mts +4 -0
package/bin/commands/server.js
CHANGED
|
@@ -10,11 +10,13 @@ exports.resolveManifestSources = resolveManifestSources;
|
|
|
10
10
|
exports.server = server;
|
|
11
11
|
var fs = _interopRequireWildcard(require("fs"));
|
|
12
12
|
var path = _interopRequireWildcard(require("path"));
|
|
13
|
+
var _crypto = require("crypto");
|
|
13
14
|
var _nodeDapi = require("../utils/node-dapi");
|
|
14
15
|
var _serverClient = require("../utils/server-client");
|
|
15
16
|
var _serverOutput = require("../utils/server-output");
|
|
16
17
|
var _serverMigrate = require("./server-migrate");
|
|
17
18
|
var _serverDomains = require("./server-domains");
|
|
19
|
+
var _serverObserve = require("./server-observe");
|
|
18
20
|
var _registry = require("../utils/migrate/registry");
|
|
19
21
|
var _walker = require("../utils/migrate/walker");
|
|
20
22
|
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); }
|
|
@@ -27,7 +29,7 @@ const ENTITY_TYPES = {
|
|
|
27
29
|
reports: 'UserReport'
|
|
28
30
|
};
|
|
29
31
|
const ENTITIES = ['users', 'groups', 'functions', 'connections', 'queries', 'scripts', 'packages', 'reports', 'files', 'tables'];
|
|
30
|
-
const COMMANDS = ['shares', 'domains', 'raw', 'batch', 'describe', 'healthcheck', 'sync', 'pull', 'push', 'migrate', 'diff', 'bundle', 'token'];
|
|
32
|
+
const COMMANDS = ['shares', 'domains', 'raw', 'batch', 'describe', 'healthcheck', 'status', 'sync', 'pull', 'push', 'migrate', 'diff', 'bundle', 'token', 'observe', 'o'];
|
|
31
33
|
const VERBS = ['list', 'count', 'get', 'delete'];
|
|
32
34
|
async function server(argv) {
|
|
33
35
|
const args = argv['_'].slice(1);
|
|
@@ -45,9 +47,18 @@ async function server(argv) {
|
|
|
45
47
|
console.log(HELP_SERVER);
|
|
46
48
|
return true;
|
|
47
49
|
}
|
|
50
|
+
if ((entity === 'observe' || entity === 'o') && !verb) {
|
|
51
|
+
console.log(_serverObserve.OBSERVE_USAGE);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
48
54
|
let client;
|
|
55
|
+
const logSpec = (0, _serverClient.logSpecOf)(argv);
|
|
56
|
+
const logId = logSpec || entity === 'functions' && verb === 'run' && isDebug(argv) ? (0, _crypto.randomUUID)() : undefined;
|
|
49
57
|
try {
|
|
50
|
-
client = await (0, _serverClient.createClient)(host, !!argv.admin
|
|
58
|
+
client = await (0, _serverClient.createClient)(host, !!argv.admin, logId ? {
|
|
59
|
+
logId,
|
|
60
|
+
spec: logSpec
|
|
61
|
+
} : undefined);
|
|
51
62
|
} catch (err) {
|
|
52
63
|
// a bad alias, URL or key is not a usage error: no help dump, just the reason
|
|
53
64
|
(0, _serverOutput.printError)(err);
|
|
@@ -60,6 +71,7 @@ async function server(argv) {
|
|
|
60
71
|
// escaping to grok.js as an unhandled rejection (stack trace + exit 255)
|
|
61
72
|
if (['pull', 'push', 'migrate', 'diff', 'bundle'].includes(entity)) return await (0, _serverMigrate.handleMigrate)(dapi, entity, [verb, ...rest].filter(Boolean), argv, output);
|
|
62
73
|
if (entity === 'domains') return await (0, _serverDomains.handleDomains)(dapi, verb, rest, argv, output);
|
|
74
|
+
if (entity === 'observe' || entity === 'o') return await (0, _serverObserve.handleObserve)(dapi, verb, rest[0], rest.slice(1), argv, output);
|
|
63
75
|
if (entity === 'batch') return await handleBatch(dapi, argv, verb, rest, output);
|
|
64
76
|
// Shell scripts that used to curl /users/login/dev get a token the same way
|
|
65
77
|
// every other command does, whatever credential the config holds.
|
|
@@ -70,6 +82,7 @@ async function server(argv) {
|
|
|
70
82
|
if (entity === 'raw') return await handleRaw(dapi, verb, rest, argv, output);
|
|
71
83
|
if (entity === 'describe') return await handleDescribe(dapi, verb ?? rest[0], output);
|
|
72
84
|
if (entity === 'healthcheck') return await handleHealthcheck(dapi, argv, output);
|
|
85
|
+
if (entity === 'status') return await handleStatus(dapi, output);
|
|
73
86
|
if (entity === 'sync') return await handleSync(dapi, verb, rest, argv, output);
|
|
74
87
|
if (entity === 'functions' && verb === 'run') return await handleFuncRun(dapi, rest, argv, output);
|
|
75
88
|
if (entity === 'functions' && verb === 'list') return await handleFunctionsList(dapi, argv, limit, offset, filter, output);
|
|
@@ -178,8 +191,15 @@ async function server(argv) {
|
|
|
178
191
|
});
|
|
179
192
|
process.exitCode = 1;
|
|
180
193
|
return true;
|
|
194
|
+
} finally {
|
|
195
|
+
if (logId) await (0, _serverObserve.printRequestLog)(dapi, logId, output);
|
|
181
196
|
}
|
|
182
197
|
}
|
|
198
|
+
|
|
199
|
+
/** `--debug` is not a declared boolean (grok publish reads it too), so it can arrive as a string. */
|
|
200
|
+
function isDebug(argv) {
|
|
201
|
+
return argv.debug !== undefined && argv.debug !== false && argv.debug !== 'false';
|
|
202
|
+
}
|
|
183
203
|
async function handleFilesPut(dapi, rest, output) {
|
|
184
204
|
const [localPath, remotePath] = rest;
|
|
185
205
|
if (!localPath || !remotePath) {
|
|
@@ -592,6 +612,39 @@ async function handleHealthcheck(dapi, argv, output) {
|
|
|
592
612
|
if (services.length) (0, _serverOutput.printOutput)(services, output);else if (module) throw new Error(`Module '${module}' is not reported by the server`);else if (output !== 'quiet') console.log('(no services reported)');
|
|
593
613
|
return true;
|
|
594
614
|
}
|
|
615
|
+
async function handleStatus(dapi, output) {
|
|
616
|
+
const [info, extended] = await Promise.all([dapi.serverStatus(), dapi.serverStatus(true)]);
|
|
617
|
+
if (output === 'json') {
|
|
618
|
+
(0, _serverOutput.printOutput)({
|
|
619
|
+
server: info,
|
|
620
|
+
extended
|
|
621
|
+
}, output);
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
if (output === 'quiet') {
|
|
625
|
+
console.log(info?.Version ?? '');
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
const http = info?.['Http Server'];
|
|
629
|
+
const fields = {
|
|
630
|
+
...info,
|
|
631
|
+
'Http Server': undefined
|
|
632
|
+
};
|
|
633
|
+
const width = Math.max(...Object.keys(fields).map(k => k.length)) + 2;
|
|
634
|
+
for (const [k, v] of Object.entries(fields)) if (v !== undefined) console.log(`${(k + ':').padEnd(width)}${v ?? ''}`);
|
|
635
|
+
if (http) console.log(`${'Http Server:'.padEnd(width)}total ${http.total}, active ${http.active}, idle ${http.idle}, closing ${http.closing}`);
|
|
636
|
+
// Non-admins get the plain count back; admins get {"METHOD url": inFlight}.
|
|
637
|
+
const active = extended?.['Http Server']?.active;
|
|
638
|
+
if (active && typeof active === 'object') {
|
|
639
|
+
console.log('');
|
|
640
|
+
const rows = Object.entries(active).map(([request, count]) => ({
|
|
641
|
+
request,
|
|
642
|
+
count
|
|
643
|
+
}));
|
|
644
|
+
if (rows.length) (0, _serverOutput.printOutput)(rows, output);else console.log('(no requests in flight)');
|
|
645
|
+
}
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
595
648
|
async function handleDescribe(dapi, entityType, output) {
|
|
596
649
|
if (!entityType) {
|
|
597
650
|
(0, _serverOutput.printError)(new Error('Usage: grok s describe <entity|type> e.g. grok s describe connections, grok s describe Project'));
|
|
@@ -611,9 +664,11 @@ async function handleDescribe(dapi, entityType, output) {
|
|
|
611
664
|
return true;
|
|
612
665
|
}
|
|
613
666
|
async function handleFuncRun(dapi, rest, argv, output) {
|
|
614
|
-
|
|
667
|
+
// `--debug 'Pkg:f()'` hands the call to --debug.
|
|
668
|
+
let funcName = rest[0] ?? (typeof argv.debug === 'string' ? argv.debug : undefined);
|
|
669
|
+
const debug = isDebug(argv);
|
|
615
670
|
if (!funcName) {
|
|
616
|
-
(0, _serverOutput.printError)(new Error('Usage: grok s functions run <Name:function(args...)> [--json params.json]'));
|
|
671
|
+
(0, _serverOutput.printError)(new Error('Usage: grok s functions run <Name:function(args...)> [--json params.json] [--debug]'));
|
|
617
672
|
return false;
|
|
618
673
|
}
|
|
619
674
|
let params = {};
|
|
@@ -631,7 +686,7 @@ async function handleFuncRun(dapi, rest, argv, output) {
|
|
|
631
686
|
params = parsed.params;
|
|
632
687
|
if (Object.keys(params).some(k => /^\d+$/.test(k))) params = (0, _nodeDapi.mapPositionalParams)(params, (await dapi.functions.find(funcName))?.parameterInfos, funcName);
|
|
633
688
|
}
|
|
634
|
-
const result = await dapi.functions.run(funcName, params);
|
|
689
|
+
const result = await dapi.functions.run(funcName, params, debug);
|
|
635
690
|
(0, _serverOutput.printOutput)(result, output);
|
|
636
691
|
return true;
|
|
637
692
|
}
|
|
@@ -927,6 +982,7 @@ Manage a Datagrok server from the command line.
|
|
|
927
982
|
Entities:
|
|
928
983
|
users, groups, functions, connections, queries, scripts, packages, reports, files, tables
|
|
929
984
|
(plus domains, shares, batch, raw, describe, healthcheck, sync, pull/push/migrate/diff/bundle below)
|
|
985
|
+
observe (alias o): problems, problem rules, logger settings, errors, timeline, cloud logs; grok s observe prints its verbs
|
|
930
986
|
|
|
931
987
|
Verbs:
|
|
932
988
|
list List entities (--filter, --limit, --offset)
|
|
@@ -946,6 +1002,8 @@ Special commands:
|
|
|
946
1002
|
grok s raw <METHOD> <path> [--json f | --data j] Any API endpoint; path is API-relative (/users/current), /api prefix optional
|
|
947
1003
|
grok s describe <entity|type> Fields of an entity type (registry record + a live sample)
|
|
948
1004
|
grok s healthcheck [--module <name>] Check server + per-module health
|
|
1005
|
+
grok s status Server build, uptime, DB pool and HTTP connections
|
|
1006
|
+
(/info/server); admins also see requests in flight
|
|
949
1007
|
grok s shares add <entity> <group>[,<group>...] [--access View|Edit]
|
|
950
1008
|
Share an entity with one or more groups
|
|
951
1009
|
grok s shares list <entity-id-or-name> List who an entity is shared with
|
|
@@ -1050,6 +1108,13 @@ Options:
|
|
|
1050
1108
|
--offset <n> Start offset (default: 0)
|
|
1051
1109
|
-r, --recursive Recursive (for files list)
|
|
1052
1110
|
--verbose Print the stack of a runtime failure, not just its message
|
|
1111
|
+
--log [<spec>] Keep what this command's requests log at <spec>, then collect it from
|
|
1112
|
+
every server instance and print it to stderr: verbose (default),
|
|
1113
|
+
standard, levels (error, warning, info, debug, audit, usage) and debug
|
|
1114
|
+
flags (db, storage, credentials, ..., or all), comma-separated. Only
|
|
1115
|
+
these requests are affected; no permission is needed
|
|
1116
|
+
--debug functions run: a debug call, as the query debugger runs it; its log (the
|
|
1117
|
+
query's stages) prints the same way
|
|
1053
1118
|
--json <file> Read a JSON body from a file (save, functions run, batch, raw)
|
|
1054
1119
|
--data '<json>' Inline JSON body for raw
|
|
1055
1120
|
-O, --output-file Write table download to a file instead of stdout
|
|
@@ -1080,6 +1145,8 @@ Examples:
|
|
|
1080
1145
|
grok s connections test --json conn.json
|
|
1081
1146
|
grok s functions run 'Chem:smilesToMw("ccc")'
|
|
1082
1147
|
grok s functions run Chem:test --json params.json
|
|
1148
|
+
grok s functions run 'MyPkg:MyQuery()' --debug # the query's own log
|
|
1149
|
+
grok s functions run UsageAnalysis:Groups --log verbose,db # and every SQL statement it ran
|
|
1083
1150
|
grok s functions list --type script --language python --limit 20
|
|
1084
1151
|
grok s functions list --package Chem --type query
|
|
1085
1152
|
grok s functions list --package PowerPack --type package --filter 'name contains "grid"'
|
|
@@ -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
|
+
}
|