github-update-submodule 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 [Muhammad Saad Amin] @SENODROOM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # github-update-submodule
2
+
3
+ > Recursively pull all Git submodules to their latest remote commit and push the updated refs up every parent repo — so **GitHub always points to the latest commit** in every submodule, no matter how deeply nested.
4
+
5
+ ---
6
+
7
+ ## The Problem
8
+
9
+ Git submodules work by storing a **commit pointer** (a hash) in the parent repo. When a submodule gets new commits, the parent repo's pointer goes stale — GitHub still shows the old commit until someone manually updates and pushes it. With deeply nested submodules this becomes a nightmare to manage by hand.
10
+
11
+ ```
12
+ GitHub (parent repo) ──pins──▶ old commit ❌
13
+ Your local submodule latest commit ✅
14
+ ```
15
+
16
+ ## The Solution
17
+
18
+ One command. Run it in any repo with submodules and every parent on GitHub will point to the latest commit — automatically, recursively, all the way down the tree.
19
+
20
+ ```bash
21
+ github-update-submodule
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ npm install -g github-update-submodule
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Usage
35
+
36
+ Navigate to your root repo in the terminal and run:
37
+
38
+ ```bash
39
+ github-update-submodule
40
+ ```
41
+
42
+ That's it. The tool will:
43
+
44
+ 1. **Pull** — fetch every submodule (at any nesting depth) and reset it to the latest commit on its remote branch
45
+ 2. **Commit** — stage and commit the updated submodule pointers in each parent repo
46
+ 3. **Push** — push from the innermost repos outward to the root, so GitHub is fully up to date at every level
47
+
48
+ ### Options
49
+
50
+ | Flag | Description |
51
+ |---|---|
52
+ | `--no-push` | Pull locally only, do not commit or push |
53
+ | `--dry-run` | Preview what would change without touching anything |
54
+ | `--message <m>` | Custom commit message (default: `chore: update submodule refs`) |
55
+ | `--branch <b>` | Default branch if not declared in `.gitmodules` (default: `main`) |
56
+ | `--depth <n>` | Limit recursion depth |
57
+ | `--verbose` | Show full git output for every operation |
58
+ | `--no-color` | Disable colored output |
59
+
60
+ ### Examples
61
+
62
+ ```bash
63
+ # Standard usage — pull + commit + push everything
64
+ github-update-submodule
65
+
66
+ # Preview changes without modifying anything
67
+ github-update-submodule --dry-run
68
+
69
+ # Pull locally only, skip the push
70
+ github-update-submodule --no-push
71
+
72
+ # Custom commit message
73
+ github-update-submodule --message "ci: bump all submodule refs to latest"
74
+
75
+ # Run on a specific repo path
76
+ github-update-submodule /path/to/your/repo
77
+
78
+ # Use master as the default branch
79
+ github-update-submodule --branch master
80
+
81
+ # Limit to 2 levels of nesting
82
+ github-update-submodule --depth 2
83
+ ```
84
+
85
+ ---
86
+
87
+ ## How It Works
88
+
89
+ ### Phase 1 — Pull
90
+
91
+ For each submodule (recursively):
92
+
93
+ 1. Initialises any submodule that hasn't been cloned yet
94
+ 2. Runs `git fetch --prune origin`
95
+ 3. Resolves the correct branch (from `.gitmodules`, then remote HEAD, then `--branch` flag)
96
+ 4. Runs `git checkout -B <branch> origin/<branch>` to hard-move to the remote tip
97
+ 5. Stages the updated pointer in the parent repo with `git add <path>`
98
+ 6. Recurses into the submodule's own submodules
99
+
100
+ ### Phase 2 — Commit & Push
101
+
102
+ Walks the repo tree **innermost → outermost**:
103
+
104
+ 1. For each repo that has staged changes, commits with the configured message
105
+ 2. Pushes to `origin/<branch>`
106
+ 3. Moves up to the parent and repeats
107
+
108
+ The innermost-first order ensures that by the time GitHub receives a pointer update from a parent repo, the commit it points to already exists on the remote.
109
+
110
+ ---
111
+
112
+ ## Example Output
113
+
114
+ ```
115
+ ╔══════════════════════════════════════════╗
116
+ ║ github-update-submodule ║
117
+ ╚══════════════════════════════════════════╝
118
+
119
+ › Repository : /projects/my-app
120
+ › Default branch : main
121
+ › Push mode : ON
122
+
123
+ Phase 1 — Pull all submodules to latest remote commit
124
+
125
+ ▸ QuantumDocsSyncer (docs/QuantumDocsSyncer)
126
+ › Fetching from origin…
127
+ › Branch: main
128
+ ✔ Updated d11a9fce → 4a82bc91
129
+ ▸ frontend (frontend)
130
+ › Fetching from origin…
131
+ › Branch: main
132
+ ✔ Updated fe03e5be → 9c14d7aa
133
+ ▸ backend (backend)
134
+ › Fetching from origin…
135
+ › Branch: main
136
+ ✔ Already up to date (b6732bc5)
137
+
138
+ Phase 2 — Commit & push updated refs (innermost → root)
139
+
140
+ › QuantumDocsSyncer — committing on main…
141
+ ↑ QuantumDocsSyncer → origin/main
142
+ › my-app — committing on main…
143
+ ↑ my-app → origin/main
144
+
145
+ ─────────────────────────────────────────
146
+ Summary
147
+ ✔ Updated : 2
148
+ · Up to date : 1
149
+ ↑ Committed : 2
150
+ ↑ Pushed : 2
151
+ ⚠ Skipped : 0
152
+ ✘ Failed : 0
153
+ Total : 3 (18.42s)
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Requirements
159
+
160
+ - **Node.js** >= 14
161
+ - **Git** installed and available in your PATH
162
+ - Your git remotes must be authenticated (SSH keys or credential manager) so pushes can succeed without a password prompt
163
+
164
+ ---
165
+
166
+ ## License
167
+
168
+ MIT
@@ -0,0 +1,376 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * github-update-submodule
5
+ * Recursively pulls all Git submodules to their latest remote commit,
6
+ * then commits and pushes the updated refs up every parent repo —
7
+ * so GitHub always points to the latest commit in every submodule.
8
+ *
9
+ * Usage:
10
+ * github-update-submodule [repo-path] [options]
11
+ *
12
+ * Options:
13
+ * --no-push Skip committing and pushing (local update only)
14
+ * --message <m> Commit message (default: "chore: update submodule refs")
15
+ * --dry-run Show what would happen without making any changes
16
+ * --branch <b> Default branch when none is declared in .gitmodules (default: main)
17
+ * --depth <n> Max recursion depth (default: unlimited)
18
+ * --verbose Show full git output
19
+ * --no-color Disable colored output
20
+ *
21
+ * Examples:
22
+ * github-update-submodule
23
+ * github-update-submodule --dry-run
24
+ * github-update-submodule --no-push
25
+ * github-update-submodule --message "ci: bump submodules"
26
+ * github-update-submodule /path/to/repo --branch master
27
+ */
28
+
29
+ const { spawnSync } = require("child_process");
30
+ const path = require("path");
31
+ const fs = require("fs");
32
+
33
+ // ─── CLI argument parsing ────────────────────────────────────────────────────
34
+
35
+ const args = process.argv.slice(2);
36
+
37
+ const options = {
38
+ repoPath: process.cwd(),
39
+ push: true, // ON by default
40
+ commitMessage: "chore: update submodule refs",
41
+ dryRun: false,
42
+ defaultBranch: "main",
43
+ maxDepth: Infinity,
44
+ verbose: false,
45
+ color: true,
46
+ };
47
+
48
+ for (let i = 0; i < args.length; i++) {
49
+ const arg = args[i];
50
+ if (arg === "--no-push") options.push = false;
51
+ else if (arg === "--dry-run") options.dryRun = true;
52
+ else if (arg === "--verbose") options.verbose = true;
53
+ else if (arg === "--no-color") options.color = false;
54
+ else if (arg === "--branch") options.defaultBranch = args[++i];
55
+ else if (arg === "--message") options.commitMessage = args[++i];
56
+ else if (arg === "--depth") options.maxDepth = parseInt(args[++i], 10);
57
+ else if (!arg.startsWith("--")) options.repoPath = path.resolve(arg);
58
+ }
59
+
60
+ // ─── Colour helpers ──────────────────────────────────────────────────────────
61
+
62
+ const C = options.color
63
+ ? { reset:"\x1b[0m", bold:"\x1b[1m", dim:"\x1b[2m", green:"\x1b[32m",
64
+ yellow:"\x1b[33m", cyan:"\x1b[36m", red:"\x1b[31m", magenta:"\x1b[35m", blue:"\x1b[34m" }
65
+ : Object.fromEntries(
66
+ ["reset","bold","dim","green","yellow","cyan","red","magenta","blue"].map(k => [k, ""])
67
+ );
68
+
69
+ // ─── Logging ─────────────────────────────────────────────────────────────────
70
+
71
+ const indent = (d) => " ".repeat(d);
72
+ const log = (d, sym, col, msg) => console.log(`${indent(d)}${col}${sym} ${msg}${C.reset}`);
73
+ const info = (d, m) => log(d, "›", C.cyan, m);
74
+ const success = (d, m) => log(d, "✔", C.green, m);
75
+ const warn = (d, m) => log(d, "⚠", C.yellow, m);
76
+ const error = (d, m) => log(d, "✘", C.red, m);
77
+ const header = (d, m) => log(d, "▸", C.bold + C.magenta, m);
78
+ const pushLog = (d, m) => log(d, "↑", C.bold + C.green, m);
79
+ const verbose = (d, m) => { if (options.verbose) log(d, " ", C.dim, m); };
80
+
81
+ // ─── Git helpers ─────────────────────────────────────────────────────────────
82
+
83
+ function git(cwd, ...gitArgs) {
84
+ const r = spawnSync("git", gitArgs, {
85
+ cwd,
86
+ encoding: "utf8",
87
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
88
+ });
89
+ return {
90
+ stdout: (r.stdout || "").trim(),
91
+ stderr: (r.stderr || "").trim(),
92
+ ok: r.status === 0,
93
+ };
94
+ }
95
+
96
+ function isGitRepo(dir) {
97
+ return fs.existsSync(path.join(dir, ".git"));
98
+ }
99
+
100
+ function parseGitmodules(repoDir) {
101
+ const gmPath = path.join(repoDir, ".gitmodules");
102
+ if (!fs.existsSync(gmPath)) return [];
103
+
104
+ const submodules = [];
105
+ let cur = null;
106
+
107
+ for (const rawLine of fs.readFileSync(gmPath, "utf8").split("\n")) {
108
+ const line = rawLine.trim();
109
+ if (!line || line.startsWith("#")) continue;
110
+
111
+ const sec = line.match(/^\[submodule\s+"(.+)"\]$/);
112
+ if (sec) {
113
+ cur = { name: sec[1], path: null, url: null, branch: null };
114
+ submodules.push(cur);
115
+ continue;
116
+ }
117
+ if (!cur) continue;
118
+
119
+ const kv = line.match(/^(\w+)\s*=\s*(.+)$/);
120
+ if (kv) {
121
+ if (kv[1] === "path") cur.path = kv[2];
122
+ if (kv[1] === "url") cur.url = kv[2];
123
+ if (kv[1] === "branch") cur.branch = kv[2];
124
+ }
125
+ }
126
+ return submodules.filter(s => s.path);
127
+ }
128
+
129
+ function resolveBranch(dir, declared) {
130
+ if (declared) return declared;
131
+ const r = git(dir, "remote", "show", "origin");
132
+ if (r.ok) {
133
+ const m = r.stdout.match(/HEAD branch:\s+(\S+)/);
134
+ if (m) return m[1];
135
+ }
136
+ return options.defaultBranch;
137
+ }
138
+
139
+ function hasStagedChanges(dir) {
140
+ return !git(dir, "diff", "--cached", "--quiet").ok;
141
+ }
142
+
143
+ // ─── Statistics ───────────────────────────────────────────────────────────────
144
+
145
+ const stats = {
146
+ updated: 0, upToDate: 0, skipped: 0, failed: 0,
147
+ committed: 0, pushed: 0, total: 0,
148
+ };
149
+
150
+ // ─── Phase 1 — pull every submodule to the remote tip ────────────────────────
151
+
152
+ function pullSubmodules(repoDir, depth = 0) {
153
+ if (depth > options.maxDepth) { warn(depth, "Max depth reached."); return false; }
154
+
155
+ const submodules = parseGitmodules(repoDir);
156
+ if (!submodules.length) { verbose(depth, "No submodules."); return false; }
157
+
158
+ let anyChanged = false;
159
+
160
+ for (const sub of submodules) {
161
+ const subDir = path.resolve(repoDir, sub.path);
162
+ stats.total++;
163
+
164
+ header(depth, `${sub.name} ${C.dim}(${sub.path})${C.reset}`);
165
+
166
+ // Init if missing
167
+ if (!fs.existsSync(subDir) || !isGitRepo(subDir)) {
168
+ info(depth + 1, "Not initialised — running git submodule update --init");
169
+ if (!options.dryRun) {
170
+ const init = git(repoDir, "submodule", "update", "--init", "--", sub.path);
171
+ if (!init.ok) {
172
+ error(depth + 1, `Init failed: ${init.stderr}`);
173
+ stats.failed++;
174
+ continue;
175
+ }
176
+ }
177
+ }
178
+
179
+ if (!fs.existsSync(subDir)) {
180
+ warn(depth + 1, "Directory missing after init — skipping.");
181
+ stats.skipped++;
182
+ continue;
183
+ }
184
+
185
+ // Fetch
186
+ info(depth + 1, "Fetching from origin…");
187
+ if (!options.dryRun) {
188
+ const f = git(subDir, "fetch", "--prune", "origin");
189
+ if (!f.ok) warn(depth + 1, `Fetch warning: ${f.stderr}`);
190
+ else verbose(depth + 1, f.stderr || "fetch ok");
191
+ }
192
+
193
+ // Resolve branch + remote tip
194
+ const branch = resolveBranch(subDir, sub.branch);
195
+ const remoteRef = `origin/${branch}`;
196
+ const remoteTip = git(subDir, "rev-parse", remoteRef).stdout;
197
+
198
+ info(depth + 1, `Branch: ${C.bold}${branch}${C.reset}`);
199
+
200
+ if (!remoteTip) {
201
+ warn(depth + 1, `Cannot resolve ${remoteRef} — skipping.`);
202
+ stats.skipped++;
203
+ continue;
204
+ }
205
+
206
+ const beforeHash = git(subDir, "rev-parse", "HEAD").stdout;
207
+
208
+ // Dry-run path
209
+ if (options.dryRun) {
210
+ if (beforeHash === remoteTip) {
211
+ success(depth + 1, `Up to date (${remoteTip.slice(0, 8)})`);
212
+ stats.upToDate++;
213
+ } else {
214
+ success(depth + 1,
215
+ `Would update ${C.dim}${beforeHash.slice(0, 8)}${C.reset} → ${C.bold}${C.green}${remoteTip.slice(0, 8)}${C.reset} (dry-run)`);
216
+ stats.updated++;
217
+ anyChanged = true;
218
+ }
219
+ pullSubmodules(subDir, depth + 1);
220
+ continue;
221
+ }
222
+
223
+ // Checkout -B <branch> <remoteRef> — moves branch pointer to remote tip
224
+ const co = git(subDir, "checkout", "-B", branch, remoteRef);
225
+ if (!co.ok) {
226
+ const co2 = git(subDir, "checkout", branch);
227
+ if (!co2.ok) {
228
+ error(depth + 1, `Cannot checkout '${branch}': ${co2.stderr}`);
229
+ stats.failed++;
230
+ continue;
231
+ }
232
+ const rs = git(subDir, "reset", "--hard", remoteRef);
233
+ if (!rs.ok) {
234
+ error(depth + 1, `reset --hard failed: ${rs.stderr}`);
235
+ stats.failed++;
236
+ continue;
237
+ }
238
+ verbose(depth + 1, rs.stdout);
239
+ } else {
240
+ verbose(depth + 1, co.stdout || co.stderr);
241
+ }
242
+
243
+ const afterHash = git(subDir, "rev-parse", "HEAD").stdout;
244
+
245
+ // Stage updated pointer in parent
246
+ git(repoDir, "add", sub.path);
247
+
248
+ if (beforeHash === afterHash) {
249
+ success(depth + 1, `Already up to date (${afterHash.slice(0, 8)})`);
250
+ stats.upToDate++;
251
+ } else {
252
+ success(depth + 1,
253
+ `Updated ${C.dim}${beforeHash.slice(0, 8)}${C.reset} → ${C.bold}${C.green}${afterHash.slice(0, 8)}${C.reset}`);
254
+ stats.updated++;
255
+ anyChanged = true;
256
+ }
257
+
258
+ // Recurse — if a nested pointer changed, re-stage this submodule in parent
259
+ if (pullSubmodules(subDir, depth + 1)) {
260
+ git(repoDir, "add", sub.path);
261
+ anyChanged = true;
262
+ }
263
+ }
264
+
265
+ return anyChanged;
266
+ }
267
+
268
+ // ─── Phase 2 — commit + push updated refs, innermost repos first ─────────────
269
+
270
+ function commitAndPush(repoDir, label, depth = 0) {
271
+ // Children first so innermost repos are pushed before outer ones
272
+ for (const sub of parseGitmodules(repoDir)) {
273
+ const subDir = path.resolve(repoDir, sub.path);
274
+ if (fs.existsSync(subDir) && isGitRepo(subDir)) {
275
+ commitAndPush(subDir, sub.name, depth + 1);
276
+ }
277
+ }
278
+
279
+ if (!hasStagedChanges(repoDir)) {
280
+ verbose(depth, `${label}: nothing staged — skipping`);
281
+ return;
282
+ }
283
+
284
+ const branch = resolveBranch(repoDir, null);
285
+ info(depth, `${C.bold}${label}${C.reset} — committing on ${C.bold}${branch}${C.reset}…`);
286
+
287
+ if (options.dryRun) {
288
+ warn(depth, `Would commit + push '${label}' → origin/${branch} (dry-run)`);
289
+ stats.committed++;
290
+ stats.pushed++;
291
+ return;
292
+ }
293
+
294
+ const commit = git(repoDir, "commit", "-m", options.commitMessage);
295
+ if (!commit.ok) {
296
+ if (/nothing to commit/.test(commit.stdout + commit.stderr)) {
297
+ verbose(depth, `${label}: nothing to commit`);
298
+ return;
299
+ }
300
+ error(depth, `Commit failed in '${label}': ${commit.stderr}`);
301
+ stats.failed++;
302
+ return;
303
+ }
304
+ verbose(depth, commit.stdout);
305
+ stats.committed++;
306
+
307
+ info(depth, `Pushing ${C.bold}${label}${C.reset} → origin/${branch}…`);
308
+ const pushR = git(repoDir, "push", "origin", branch);
309
+ if (!pushR.ok) {
310
+ error(depth, `Push failed in '${label}': ${pushR.stderr}`);
311
+ stats.failed++;
312
+ return;
313
+ }
314
+ pushLog(depth, `${C.bold}${label}${C.reset} → origin/${branch}`);
315
+ stats.pushed++;
316
+ }
317
+
318
+ // ─── Entry point ─────────────────────────────────────────────────────────────
319
+
320
+ function main() {
321
+ console.log();
322
+ console.log(`${C.bold}${C.blue}╔══════════════════════════════════════════╗${C.reset}`);
323
+ console.log(`${C.bold}${C.blue}║ github-update-submodule ║${C.reset}`);
324
+ console.log(`${C.bold}${C.blue}╚══════════════════════════════════════════╝${C.reset}`);
325
+ console.log();
326
+
327
+ if (!isGitRepo(options.repoPath)) {
328
+ error(0, `Not a git repository: ${options.repoPath}`);
329
+ process.exit(1);
330
+ }
331
+
332
+ info(0, `Repository : ${C.bold}${options.repoPath}${C.reset}`);
333
+ info(0, `Default branch : ${C.bold}${options.defaultBranch}${C.reset}`);
334
+ info(0, `Push mode : ${options.push ? C.bold + C.green + "ON" : C.dim + "OFF"}${C.reset}`);
335
+ if (options.dryRun) warn(0, "DRY RUN — no changes will be made");
336
+ if (options.maxDepth !== Infinity) info(0, `Max depth : ${options.maxDepth}`);
337
+ console.log();
338
+
339
+ const t0 = Date.now();
340
+
341
+ // Phase 1 — pull
342
+ console.log(`${C.bold}${C.cyan}Phase 1 — Pull all submodules to latest remote commit${C.reset}`);
343
+ console.log();
344
+ pullSubmodules(options.repoPath, 0);
345
+
346
+ // Phase 2 — commit + push
347
+ if (options.push) {
348
+ console.log();
349
+ console.log(`${C.bold}${C.cyan}Phase 2 — Commit & push updated refs (innermost → root)${C.reset}`);
350
+ console.log();
351
+ commitAndPush(options.repoPath, path.basename(options.repoPath), 0);
352
+ } else {
353
+ console.log();
354
+ warn(0, `Refs staged locally but NOT pushed (--no-push mode).`);
355
+ }
356
+
357
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(2);
358
+
359
+ console.log();
360
+ console.log(`${C.bold}${C.blue}─────────────────────────────────────────${C.reset}`);
361
+ console.log(`${C.bold}Summary${C.reset}`);
362
+ console.log(` ${C.green}✔ Updated : ${stats.updated}${C.reset}`);
363
+ console.log(` ${C.cyan}· Up to date : ${stats.upToDate}${C.reset}`);
364
+ if (options.push) {
365
+ console.log(` ${C.green}↑ Committed : ${stats.committed}${C.reset}`);
366
+ console.log(` ${C.green}↑ Pushed : ${stats.pushed}${C.reset}`);
367
+ }
368
+ console.log(` ${C.yellow}⚠ Skipped : ${stats.skipped}${C.reset}`);
369
+ console.log(` ${C.red}✘ Failed : ${stats.failed}${C.reset}`);
370
+ console.log(` ${C.dim} Total : ${stats.total} (${elapsed}s)${C.reset}`);
371
+ console.log();
372
+
373
+ if (stats.failed > 0) process.exit(1);
374
+ }
375
+
376
+ main();
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "github-update-submodule",
3
+ "version": "1.0.0",
4
+ "description": "Recursively pull all Git submodules to their latest remote commit and push updated refs up every parent repo — so GitHub always points to the latest.",
5
+ "main": "bin/github-update-submodule.js",
6
+ "bin": {
7
+ "github-update-submodule": "bin/github-update-submodule.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"No tests yet\" && exit 0"
11
+ },
12
+ "keywords": [
13
+ "git",
14
+ "submodule",
15
+ "github",
16
+ "recursive",
17
+ "update",
18
+ "cli"
19
+ ],
20
+ "author": "Muhammad Saad Amin",
21
+ "license": "MIT",
22
+ "engines": {
23
+ "node": ">=14.0.0"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/SENODROOM/github-update-submodule.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/SENODROOM/github-update-submodule/issues"
31
+ },
32
+ "homepage": "https://github.com/SENODROOM/github-update-submodule#readme"
33
+ }