From 3ce3f7583ba84375add147a3afddfddd80e6e225 Mon Sep 17 00:00:00 2001 From: bryanprimus Date: Mon, 20 Apr 2026 18:56:53 +0800 Subject: [PATCH 01/11] feat: add --init flag to initialize git repo after clone --- README.md | 3 +++ bin/index.ts | 17 +++++++++++++++++ tests/cli.test.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/README.md b/README.md index 91130da..351375a 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ npx gitpick owner/repo -i --dry-run - **šŸ”„ Interactive mode** - browse and cherry-pick files/folders with `-i` | `--interactive` - šŸ” Seamlessly works with both public and private repositories using a PAT - šŸ“¦ Can easily clone all submodules with `-r` | `--recursive` +- 🧱 Initialize cloned output as a new git repo with `--init` - šŸ”Ž Preview what would be cloned with `--dry-run` before cloning - 🌳 View cloned file structure as a colored tree with `--tree` - šŸ—‘ļø Overwrite or replace existing files without a prompt using `-o` | `--overwrite` @@ -115,6 +116,7 @@ npx gitpick owner/repo/blob/main/path/to/file # a file aka blob npx gitpick # default git behavior npx gitpick [target] # with optional target npx gitpick -b [branch/SHA] # branch or commit SHA +npx gitpick --init # initialize as a new git repository npx gitpick -o # overwrite if exists npx gitpick -r # clone submodules npx gitpick -w 30s # sync every 30 seconds @@ -132,6 +134,7 @@ npx gitpick https://bitbucket.org/owner/repo # Bitbucket ``` -b, --branch Branch/SHA to clone +- --init Initialize target as a new git repository -i, --interactive Browse and pick files/folders interactively -n, --dry-run Show what would be cloned without cloning -o, --overwrite Skip overwrite prompt diff --git a/bin/index.ts b/bin/index.ts index e835df6..efeeb21 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -33,6 +33,7 @@ ${bold("Arguments:")} ${bold("Options:")} ${cyan("-b, --branch ")} Branch/SHA to clone + ${cyan(" --init")} Initialize target as a new git repository ${cyan("-i, --interactive")} Browse and pick files/folders interactively ${cyan("-n, --dry-run")} Show what would be cloned without cloning ${cyan("-o, --overwrite")} Skip overwrite prompt @@ -106,6 +107,15 @@ const parse: typeof parseArgs = (config) => { } } +const initGitRepo = async (targetPath: string, type: string, options: { init?: boolean }) => { + if (!options.init) return + + const repoPath = type === "blob" ? path.dirname(targetPath) : targetPath + if (fs.existsSync(path.join(repoPath, ".git"))) return + + await spawn("git", ["init"], { cwd: repoPath }) +} + const main = async () => { scheduleUpdateCheck() @@ -117,6 +127,7 @@ const main = async () => { "dry-run": { type: "boolean", short: "n" }, force: { type: "boolean", short: "f" }, help: { type: "boolean", short: "h" }, + init: { type: "boolean" }, interactive: { type: "boolean", short: "i" }, quiet: { type: "boolean", short: "q" }, tree: { type: "boolean" }, @@ -155,6 +166,7 @@ const main = async () => { branch: values.branch, dryRun: values["dry-run"], force: values.force, + init: values.init, interactive: values.interactive, quiet: values.quiet, tree: values.tree, @@ -384,6 +396,7 @@ const main = async () => { await printTree(targetDir) process.stdout.write("\n") } + await initGitRepo(targetDir, "repository", options) process.exit(0) } @@ -565,6 +578,7 @@ const main = async () => { `āœ” Copied ${copiedFiles} file${copiedFiles !== 1 ? "s" : ""} to ${displayPath(targetPath)}`, ), ) + await initGitRepo(targetPath, "repository", options) if (options.tree) { process.stdout.write(`\n${bold(cyan(displayPath(targetPath)))}\n`) await printTree(targetPath) @@ -624,14 +638,17 @@ const main = async () => { if (!silent) console.log(`\nšŸ‘€ Watching every ${parseTimeString(options.watch) / 1000 + "s"}\n`) await cloneAction(config, options, targetPath) + await initGitRepo(targetPath, config.type, options) if (options.tree) await renderTree(targetPath) const watchInterval = parseTimeString(options.watch) setInterval(async () => { await cloneAction(config, options, targetPath) + await initGitRepo(targetPath, config.type, options) if (options.tree) await renderTree(targetPath) }, watchInterval) } else { await cloneAction(config, options, targetPath) + await initGitRepo(targetPath, config.type, options) if (options.tree) await renderTree(targetPath) notifyUpdate(version, silent) process.exit(0) diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 796f662..29ebf9c 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -1033,6 +1033,34 @@ describe("CLI flags", () => { expect(exitCode).toBe(0) expect(parseLine(output)).toContain("nrjdalal/picksuite repository:main") }, 30000) + + it("--help includes --init", async () => { + const { output, exitCode } = await run(["--help"]) + expect(exitCode).toBe(0) + expect(stripAnsi(output)).toContain("--init") + }) +}) + +describe("--init", () => { + it("initializes git repository for directory clone", async () => { + const t = target() + const { exitCode } = await run(["clone", "nrjdalal/picksuite/tree/main/folder", t, "--init"]) + expect(exitCode).toBe(0) + expect(existsSync(join(t, ".git"))).toBe(true) + }, 30000) + + it("initializes git repository in parent dir for blob clone", async () => { + const t = target() + const blobTarget = join(t, "renamed.txt") + const { exitCode } = await run([ + "clone", + "nrjdalal/picksuite/blob/main/file.txt", + blobTarget, + "--init", + ]) + expect(exitCode).toBe(0) + expect(existsSync(join(t, ".git"))).toBe(true) + }, 30000) }) // ===================================================================== From 670d62fccb413ee86c1efc2754f84965480e136d Mon Sep 17 00:00:00 2001 From: bryanprimus Date: Mon, 20 Apr 2026 19:28:19 +0800 Subject: [PATCH 02/11] feat: add --commit flag to allow initial commit with custom msg --- README.md | 3 +++ bin/index.ts | 20 ++++++++++++++++---- tests/cli.test.ts | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 351375a..7fe6ee0 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ npx gitpick owner/repo -i --dry-run - šŸ” Seamlessly works with both public and private repositories using a PAT - šŸ“¦ Can easily clone all submodules with `-r` | `--recursive` - 🧱 Initialize cloned output as a new git repo with `--init` +- šŸ’¾ Stage and create an initial git commit with `-m`/`--commit ` (implies `--init`) - šŸ”Ž Preview what would be cloned with `--dry-run` before cloning - 🌳 View cloned file structure as a colored tree with `--tree` - šŸ—‘ļø Overwrite or replace existing files without a prompt using `-o` | `--overwrite` @@ -117,6 +118,7 @@ npx gitpick # default git behavior npx gitpick [target] # with optional target npx gitpick -b [branch/SHA] # branch or commit SHA npx gitpick --init # initialize as a new git repository +npx gitpick --commit "Initial commit" # init + stage + initial commit npx gitpick -o # overwrite if exists npx gitpick -r # clone submodules npx gitpick -w 30s # sync every 30 seconds @@ -135,6 +137,7 @@ npx gitpick https://bitbucket.org/owner/repo # Bitbucket ``` -b, --branch Branch/SHA to clone - --init Initialize target as a new git repository +-m, --commit Stage all files and create initial git commit -i, --interactive Browse and pick files/folders interactively -n, --dry-run Show what would be cloned without cloning -o, --overwrite Skip overwrite prompt diff --git a/bin/index.ts b/bin/index.ts index efeeb21..665429d 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -34,6 +34,7 @@ ${bold("Arguments:")} ${bold("Options:")} ${cyan("-b, --branch ")} Branch/SHA to clone ${cyan(" --init")} Initialize target as a new git repository + ${cyan("-m, --commit ")} Stage all files and create initial git commit ${cyan("-i, --interactive")} Browse and pick files/folders interactively ${cyan("-n, --dry-run")} Show what would be cloned without cloning ${cyan("-o, --overwrite")} Skip overwrite prompt @@ -107,13 +108,22 @@ const parse: typeof parseArgs = (config) => { } } -const initGitRepo = async (targetPath: string, type: string, options: { init?: boolean }) => { - if (!options.init) return +const initGitRepo = async ( + targetPath: string, + type: string, + options: { init?: boolean; commit?: string }, +) => { + if (!options.init && !options.commit) return const repoPath = type === "blob" ? path.dirname(targetPath) : targetPath - if (fs.existsSync(path.join(repoPath, ".git"))) return + if (!fs.existsSync(path.join(repoPath, ".git"))) { + await spawn("git", ["init"], { cwd: repoPath }) + } - await spawn("git", ["init"], { cwd: repoPath }) + if (options.commit) { + await spawn("git", ["add", "."], { cwd: repoPath }) + await spawn("git", ["commit", "-m", options.commit], { cwd: repoPath }) + } } const main = async () => { @@ -128,6 +138,7 @@ const main = async () => { force: { type: "boolean", short: "f" }, help: { type: "boolean", short: "h" }, init: { type: "boolean" }, + commit: { type: "string", short: "m" }, interactive: { type: "boolean", short: "i" }, quiet: { type: "boolean", short: "q" }, tree: { type: "boolean" }, @@ -167,6 +178,7 @@ const main = async () => { dryRun: values["dry-run"], force: values.force, init: values.init, + commit: values.commit, interactive: values.interactive, quiet: values.quiet, tree: values.tree, diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 29ebf9c..ed4b572 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -1061,6 +1061,40 @@ describe("--init", () => { expect(exitCode).toBe(0) expect(existsSync(join(t, ".git"))).toBe(true) }, 30000) + + it("creates initial commit for directory clone", async () => { + const t = target() + const { exitCode } = await run([ + "clone", + "nrjdalal/picksuite/tree/main/folder", + t, + "--init", + "--commit", + "Initial commit", + ]) + expect(exitCode).toBe(0) + expect(existsSync(join(t, ".git"))).toBe(true) + const proc = Bun.spawn(["git", "log", "--oneline"], { stdout: "pipe", cwd: t }) + const log = await new Response(proc.stdout).text() + expect(log).toContain("Initial commit") + }, 30000) + + it("creates initial commit in parent dir for blob clone (--commit implies --init)", async () => { + const t = target() + const blobTarget = join(t, "renamed.txt") + const { exitCode } = await run([ + "clone", + "nrjdalal/picksuite/blob/main/file.txt", + blobTarget, + "--commit", + "feat: initial scaffold", + ]) + expect(exitCode).toBe(0) + expect(existsSync(join(t, ".git"))).toBe(true) + const proc = Bun.spawn(["git", "log", "--oneline"], { stdout: "pipe", cwd: t }) + const log = await new Response(proc.stdout).text() + expect(log).toContain("feat: initial scaffold") + }, 30000) }) // ===================================================================== From ff0d3407f5d9ba4b43a3be8d34b1b7fa55025fa4 Mon Sep 17 00:00:00 2001 From: bryanprimus Date: Wed, 22 Apr 2026 14:10:53 +0800 Subject: [PATCH 03/11] fix: remove initGitRepo call inside --watch mode since we only need to initialize and commit once --- bin/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/index.ts b/bin/index.ts index 665429d..ebac630 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -655,7 +655,6 @@ const main = async () => { const watchInterval = parseTimeString(options.watch) setInterval(async () => { await cloneAction(config, options, targetPath) - await initGitRepo(targetPath, config.type, options) if (options.tree) await renderTree(targetPath) }, watchInterval) } else { From 8f139b77aafe1cf46d162bac77b82cc5033b44da Mon Sep 17 00:00:00 2001 From: bryanprimus Date: Wed, 22 Apr 2026 14:15:17 +0800 Subject: [PATCH 04/11] feat: handle spawn error during git commit when git identity is missing --- bin/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bin/index.ts b/bin/index.ts index ebac630..50e9e49 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -56,7 +56,7 @@ ${bold("Examples:")} $ gitpick --dry-run $ gitpick https://gitlab.com/owner/repo $ gitpick https://bitbucket.org/owner/repo - + šŸš€ More awesome tools at ${cyan("https://github.com/nrjdalal")}` const displayPath = (targetPath: string) => { @@ -122,7 +122,11 @@ const initGitRepo = async ( if (options.commit) { await spawn("git", ["add", "."], { cwd: repoPath }) - await spawn("git", ["commit", "-m", options.commit], { cwd: repoPath }) + try { + await spawn("git", ["commit", "-m", options.commit], { cwd: repoPath }) + } catch { + console.log(`\nāœ– git commit failed — configure user.name / user.email`) + } } } From 243f2d0be570541b659da01257ebc29ea6f048ac Mon Sep 17 00:00:00 2001 From: bryanprimus Date: Wed, 22 Apr 2026 14:27:51 +0800 Subject: [PATCH 05/11] fix: make init possible only when there is a target directory --- bin/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bin/index.ts b/bin/index.ts index 50e9e49..08ef217 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -116,6 +116,14 @@ const initGitRepo = async ( if (!options.init && !options.commit) return const repoPath = type === "blob" ? path.dirname(targetPath) : targetPath + + if (type === "blob" && repoPath === process.cwd()) { + console.log( + `\nāœ– Skipping git init: Cannot initialize a git repository for a single file in the current working directory.`, + ) + return + } + if (!fs.existsSync(path.join(repoPath, ".git"))) { await spawn("git", ["init"], { cwd: repoPath }) } From a9aa4b8a5cf9a5f66748dfba167c7125d819c4ac Mon Sep 17 00:00:00 2001 From: bryanprimus Date: Wed, 22 Apr 2026 14:54:20 +0800 Subject: [PATCH 06/11] feat: introduce CloneType and fix unhandled init, commit for type raw and tree --- bin/index.ts | 19 ++++++++----------- bin/utils/transform-url.ts | 4 +++- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/bin/index.ts b/bin/index.ts index 08ef217..e420c03 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -108,16 +108,13 @@ const parse: typeof parseArgs = (config) => { } } -const initGitRepo = async ( - targetPath: string, - type: string, - options: { init?: boolean; commit?: string }, -) => { +const initGitRepo = async (targetPath: string, options: { init?: boolean; commit?: string }) => { if (!options.init && !options.commit) return - const repoPath = type === "blob" ? path.dirname(targetPath) : targetPath + const isFile = fs.existsSync(targetPath) && fs.statSync(targetPath).isFile() + const repoPath = isFile ? path.dirname(targetPath) : targetPath - if (type === "blob" && repoPath === process.cwd()) { + if (isFile && repoPath === process.cwd()) { console.log( `\nāœ– Skipping git init: Cannot initialize a git repository for a single file in the current working directory.`, ) @@ -420,7 +417,7 @@ const main = async () => { await printTree(targetDir) process.stdout.write("\n") } - await initGitRepo(targetDir, "repository", options) + await initGitRepo(targetDir, options) process.exit(0) } @@ -602,7 +599,7 @@ const main = async () => { `āœ” Copied ${copiedFiles} file${copiedFiles !== 1 ? "s" : ""} to ${displayPath(targetPath)}`, ), ) - await initGitRepo(targetPath, "repository", options) + await initGitRepo(targetPath, options) if (options.tree) { process.stdout.write(`\n${bold(cyan(displayPath(targetPath)))}\n`) await printTree(targetPath) @@ -662,7 +659,7 @@ const main = async () => { if (!silent) console.log(`\nšŸ‘€ Watching every ${parseTimeString(options.watch) / 1000 + "s"}\n`) await cloneAction(config, options, targetPath) - await initGitRepo(targetPath, config.type, options) + await initGitRepo(targetPath, options) if (options.tree) await renderTree(targetPath) const watchInterval = parseTimeString(options.watch) setInterval(async () => { @@ -671,7 +668,7 @@ const main = async () => { }, watchInterval) } else { await cloneAction(config, options, targetPath) - await initGitRepo(targetPath, config.type, options) + await initGitRepo(targetPath, options) if (options.tree) await renderTree(targetPath) notifyUpdate(version, silent) process.exit(0) diff --git a/bin/utils/transform-url.ts b/bin/utils/transform-url.ts index 95a5d2f..9ef84ed 100644 --- a/bin/utils/transform-url.ts +++ b/bin/utils/transform-url.ts @@ -12,6 +12,8 @@ const PREFIXES: { prefix: string; host: Host }[] = [ { prefix: "https://bitbucket.org/", host: "bitbucket.org" }, ] +export type CloneType = "raw" | "blob" | "tree" | "repository" + export async function configFromUrl( url: string, { @@ -64,7 +66,7 @@ export async function configFromUrl( const repoUrl = `https://${token ? token + "@" : token}${host}/${owner}/${repository}` - let type: string + let type: CloneType let resolvedBranch: string let resolvedPath: string From 336c8b81190a1cbdc5f4f59d2ec073e0ddf31012 Mon Sep 17 00:00:00 2001 From: bryanprimus Date: Wed, 22 Apr 2026 15:01:44 +0800 Subject: [PATCH 07/11] docs: warn about --commit staging unrelated files --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7fe6ee0..e0f4c19 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,8 @@ npx gitpick https://bitbucket.org/owner/repo # Bitbucket -v, --version display the version number ``` +> **Note on `--commit`:** When cloning a single file (`blob`) into an existing non-empty directory, the automated `git add .` command will stage and commit all unrelated files currently present in that directory alongside the cloned file. + --- ## šŸ”„ Interactive Mode From 0afaaf028ed5aa0dc1695d3eef051819aec5ad43 Mon Sep 17 00:00:00 2001 From: bryanprimus Date: Wed, 22 Apr 2026 15:06:50 +0800 Subject: [PATCH 08/11] style: align CLI help text formatting in README and index.ts --- README.md | 2 +- bin/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e0f4c19..4a57c2a 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ npx gitpick https://bitbucket.org/owner/repo # Bitbucket ``` -b, --branch Branch/SHA to clone -- --init Initialize target as a new git repository + --init Initialize target as a new git repository -m, --commit Stage all files and create initial git commit -i, --interactive Browse and pick files/folders interactively -n, --dry-run Show what would be cloned without cloning diff --git a/bin/index.ts b/bin/index.ts index e420c03..6308f95 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -34,7 +34,7 @@ ${bold("Arguments:")} ${bold("Options:")} ${cyan("-b, --branch ")} Branch/SHA to clone ${cyan(" --init")} Initialize target as a new git repository - ${cyan("-m, --commit ")} Stage all files and create initial git commit + ${cyan("-m, --commit ")} Stage all files and create initial git commit ${cyan("-i, --interactive")} Browse and pick files/folders interactively ${cyan("-n, --dry-run")} Show what would be cloned without cloning ${cyan("-o, --overwrite")} Skip overwrite prompt From 9d34b3e4e4ae5958a21722375349e34346969313 Mon Sep 17 00:00:00 2001 From: bryanprimus Date: Wed, 22 Apr 2026 15:30:39 +0800 Subject: [PATCH 09/11] feat: add default commit msg to standalone --commit flag --- bin/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bin/index.ts b/bin/index.ts index 6308f95..9f84f83 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -139,7 +139,18 @@ const main = async () => { scheduleUpdateCheck() try { + // parseArgs lacks optional strings. To stay zero-dependency, we inject + // a default "init awesomeness" when --commit is passed without a value. + const args = process.argv.slice(2).reduce((acc, arg, i, arr) => { + acc.push(arg) + if ((arg === "-m" || arg === "--commit") && (!arr[i + 1] || arr[i + 1].startsWith("-"))) { + acc.push("init awesomeness") + } + return acc + }, [] as string[]) + const { positionals, values } = parse({ + args, allowPositionals: true, options: { branch: { type: "string", short: "b" }, From fd369c5b002dc4081f6589e16c1e09755a2083f5 Mon Sep 17 00:00:00 2001 From: bryanprimus Date: Wed, 22 Apr 2026 15:43:17 +0800 Subject: [PATCH 10/11] test: update test --- tests/cli.test.ts | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/cli.test.ts b/tests/cli.test.ts index ed4b572..6c5afea 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -1095,6 +1095,64 @@ describe("--init", () => { const log = await new Response(proc.stdout).text() expect(log).toContain("feat: initial scaffold") }, 30000) + + it("idempotency: calling --init when .git already exists should not error", async () => { + const t = target() + mkdirSync(join(t, ".git"), { recursive: true }) + const { exitCode } = await run([ + "clone", + "nrjdalal/picksuite/tree/main/folder", + t, + "--init", + "-o", + ]) + expect(exitCode).toBe(0) + }, 30000) + + it("--commit without git user config — confirms the error surface", async () => { + const t = target() + const proc = Bun.spawn( + [...CLI, "clone", "nrjdalal/picksuite/tree/main/folder", t, "--commit", "Init"], + { + stdout: "pipe", + stderr: "pipe", + env: { + ...process.env, + GIT_AUTHOR_NAME: "", + GIT_COMMITTER_NAME: "", + GIT_AUTHOR_EMAIL: "", + GIT_COMMITTER_EMAIL: "", + }, + }, + ) + const [stdout, stderr] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]) + await proc.exited + expect(stdout + stderr).toMatch(/user\.(name|email)/i) + }, 30000) +}) + +describe("watch mode", () => { + it("assert no crash on the second tick", async () => { + const t = target() + const proc = Bun.spawn( + [...CLI, "clone", "nrjdalal/picksuite/tree/main/folder", t, "-w", "1s"], + { + stdout: "pipe", + stderr: "pipe", + }, + ) + + // Wait long enough for a second tick + await new Promise((resolve) => setTimeout(resolve, 1500)) + proc.kill() + const stdout = await new Response(proc.stdout).text() + + // Assert that it didn't crash before being killed + expect(stdout).not.toMatch(/error:/i) + }, 30000) }) // ===================================================================== From e67e7b1dbe62311a2eff2f312c35427bbebd7e09 Mon Sep 17 00:00:00 2001 From: bryanprimus Date: Wed, 22 Apr 2026 15:49:34 +0800 Subject: [PATCH 11/11] docs: prevent layout shift on examples --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a57c2a..24af825 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ npx gitpick # default git behavior npx gitpick [target] # with optional target npx gitpick -b [branch/SHA] # branch or commit SHA npx gitpick --init # initialize as a new git repository -npx gitpick --commit "Initial commit" # init + stage + initial commit +npx gitpick --commit "init commit" # init + stage + initial commit npx gitpick -o # overwrite if exists npx gitpick -r # clone submodules npx gitpick -w 30s # sync every 30 seconds