oneclickpush 0.2.0 → 0.4.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.
Files changed (5) hide show
  1. package/README.md +55 -33
  2. package/cli.js +84 -36
  3. package/index.d.ts +23 -54
  4. package/index.js +19 -21
  5. package/package.json +3 -2
package/README.md CHANGED
@@ -16,27 +16,28 @@ none of these, has zero dependencies and only uses built-in Node APIs + `git`.
16
16
  Deploy your build directory (e.g. `dist`) to the `gh-pages` branch:
17
17
 
18
18
  ```text
19
- $ cd dist
20
- $ npx oneclickpush
19
+ $ npx oneclickpush dist
21
20
  ```
22
21
 
23
22
  ## CLI Usage
24
23
 
25
24
  ```text
26
- Usage: oneclickpush [options]
25
+ Usage: oneclickpush <dir> [options]
27
26
 
28
27
  Replace the contents of a remote git branch with that of a local directory.
29
28
 
29
+ Arguments:
30
+ dir Directory to deploy (required)
31
+
30
32
  Options:
31
- -d, --dist <dir> Directory to publish (default: ".")
32
- -e, --exclude <glob> Paths to leave out, relative to dist (default: "**/.DS_Store")
33
+ -e, --exclude <glob> Paths to leave out, relative to dir (default: "**/.DS_Store")
33
34
  -b, --branch <name> Target branch (default: "gh-pages")
34
35
  -m, --message <msg> Commit message (default: "Updates")
35
- -r, --remote <name|url> Remote name or repo URL to publish to (default: "origin")
36
+ -r, --remote <name|url> Remote name or repo URL to deploy to (default: "origin")
36
37
  -a, --author <author> Commit author, as "Name <email>"
37
38
  -f, --force Force push new commit with no history
38
39
  -n, --dry-run Show what would change, without committing or pushing
39
- -v, --verbose Log git commands
40
+ -l, --log-level <level> Output: silent, normal or verbose (default: "normal")
40
41
  -V, --version Print version
41
42
  -h, --help Show this help
42
43
  ```
@@ -45,27 +46,46 @@ Options:
45
46
  > `--exclude` takes glob patterns (`*`, `**`, `?`) and can be repeated. Patterns are matched against
46
47
  > the full path, so use `**/name` to match at any depth.
47
48
 
49
+ ### Log levels
50
+
51
+ | Level | Behavior |
52
+ | --------- | -------------------------------------------------------------------- |
53
+ | `silent` | Prints nothing, not even errors. Check the exit code instead. |
54
+ | `normal` | Prints the result and error messages (plus a spinner in a terminal). |
55
+ | `verbose` | Also logs every git command to stderr and prints full errors. |
56
+
57
+ > [!WARNING]
58
+ > If your remote URL contains a token (e.g. `https://user:TOKEN@github.com/o/r.git`), git error
59
+ > messages shown at `normal` and `verbose` may include it. Use `-l silent` in CI, or keep tokens out
60
+ > of the URL with a credential helper.
61
+
48
62
  ### Examples
49
63
 
50
64
  ```bash
51
65
  # Preview what would be deployed without pushing
52
- npx oneclickpush -d build --dry-run
66
+ npx oneclickpush build --dry-run
53
67
 
54
68
  # Custom branch and commit message
55
- npx oneclickpush -d public -b release -m "deploy: v1.0.0"
69
+ npx oneclickpush public -b release -m "deploy: v1.0.0"
56
70
 
57
71
  # Exclude source maps and test files
58
- npx oneclickpush -d dist -e "**/*.map" -e "**/tests/**"
72
+ npx oneclickpush dist -e "**/*.map" -e "**/tests/**"
59
73
 
60
74
  # Deploy to a different remote or repository URL
61
- npx oneclickpush -d dist -r upstream
62
- npx oneclickpush -d dist -r git@github.com:user/repo.git
75
+ npx oneclickpush dist -r upstream
76
+ npx oneclickpush dist -r git@github.com:user/repo.git
63
77
 
64
78
  # Set commit author (useful in CI environments)
65
- npx oneclickpush -d dist -a "Deploy Bot <bot@example.com>"
79
+ npx oneclickpush dist -a "Deploy Bot <bot@example.com>"
66
80
 
67
81
  # Force push fresh history
68
- npx oneclickpush -d dist --force
82
+ npx oneclickpush dist --force
83
+
84
+ # Silent output (useful in CI, check the exit code)
85
+ npx oneclickpush dist -l silent
86
+
87
+ # Log every git command
88
+ npx oneclickpush dist -l verbose
69
89
  ```
70
90
 
71
91
  ## Programmatic API
@@ -73,15 +93,15 @@ npx oneclickpush -d dist --force
73
93
  You can also use `oneclickpush` as an ES module:
74
94
 
75
95
  ```bash
76
- # Install the library
96
+ # Install
77
97
  npm i oneclickpush
78
98
  ```
79
99
 
80
100
  ```javascript
81
- import { publish } from 'oneclickpush'
101
+ import { deploy } from 'oneclickpush'
82
102
 
83
- const changes = await publish({
84
- dist: 'dist',
103
+ const changes = await deploy({
104
+ dir: 'dist',
85
105
  exclude: ['**/.DS_Store', '**/*.map'],
86
106
  branch: 'gh-pages',
87
107
  message: 'Deploy updates',
@@ -93,25 +113,25 @@ const changes = await publish({
93
113
  })
94
114
 
95
115
  if (changes) {
96
- console.log('Published changes:\n' + changes)
116
+ console.log('Deployed changes:\n' + changes)
97
117
  } else {
98
- console.log('Nothing to publish')
118
+ console.log('Nothing to deploy')
99
119
  }
100
120
  ```
101
121
 
102
122
  ### Options
103
123
 
104
- | Option | Type | Default | Description |
124
+ | Name | Type | Default | Description |
105
125
  | --------- | ---------- | ------------------ | --------------------------------------------------- |
106
- | `dist` | `string` | `"."` | Directory to publish |
107
- | `exclude` | `string[]` | `["**/.DS_Store"]` | Paths to exclude relative to `dist` |
126
+ | `dir` | `string` | - (required) | Directory to deploy |
127
+ | `exclude` | `string[]` | `["**/.DS_Store"]` | Paths to exclude relative to `dir` |
108
128
  | `branch` | `string` | `"gh-pages"` | Target git branch |
109
129
  | `message` | `string` | `"Updates"` | Git commit message |
110
- | `remote` | `string` | `"origin"` | Remote name or repository URL/path to publish to |
111
- | `author` | `string` | — | Commit author and committer, as `"Name <email>"` |
130
+ | `remote` | `string` | `"origin"` | Remote name or repository URL/path to deploy to |
131
+ | `author` | `string` | - | Commit author and committer, as `"Name <email>"` |
112
132
  | `force` | `boolean` | `false` | Force push fresh commit with no previous history |
113
133
  | `dryRun` | `boolean` | `false` | Return staged changes without committing or pushing |
114
- | `verbose` | `boolean` | `false` | Log executed git commands |
134
+ | `verbose` | `boolean` | `false` | Logs git commands to stderr |
115
135
 
116
136
  ## How It Works
117
137
 
@@ -119,7 +139,7 @@ if (changes) {
119
139
  URL/path directly.
120
140
  2. Creates an isolated temporary directory and fetches the target branch (or initializes it if it
121
141
  doesn't exist).
122
- 3. Clears the target branch index and stages the contents of `dist` (minus `exclude` patterns)
142
+ 3. Clears the target branch index and stages the contents of `dir` (minus `exclude` patterns)
123
143
  directly via Git `work-tree`.
124
144
  4. Checks for differences (`git diff --cached --name-status`). If there are changes, commits and
125
145
  pushes back to the remote repository.
@@ -130,12 +150,14 @@ if (changes) {
130
150
 
131
151
  ### Notes
132
152
 
133
- - Dotfiles (except `.DS_Store`) are included by default; use `-e` to exclude.
134
- - Contents in target branch are emptied and replaced with the contents of `dist` (WYSIWYG).
135
- - If `.nojekyll` or `CNAME` files are required, add them to `dist`; use a post-build step if needed.
136
- - If the target branch does not exist, it will be created.
137
- - When `--remote` is a repository URL or path, running from within an existing git repository is not
138
- required.
153
+ - The target branch is fully replaced with the contents of `dir` (WYSIWYG), and it's created if it
154
+ doesn't exist yet. If you need files like `.nojekyll` or `CNAME`, put them in `dir`, adding a
155
+ post-build step if your build tool wipes the directory.
156
+ - Dotfiles are included by default, except `.DS_Store`. Use `-e` to exclude more, but note that it
157
+ replaces the default list, so repeat `-e "**/.DS_Store"` if you still want that file excluded.
158
+ - When `--remote` is a repository URL or path, you don't need to run from inside a git repository.
159
+ - In dry-run output, identical content moved to a new path appears as a rename (`R100`).
160
+ - A directory starting with `-` must come after `--`, e.g. `oneclickpush -- -weird-dir`.
139
161
 
140
162
  ## License
141
163
 
package/cli.js CHANGED
@@ -1,48 +1,77 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'node:util'
3
3
  import { readFileSync } from 'node:fs'
4
- import { publish } from './index.js'
4
+ import { deploy } from './index.js'
5
5
 
6
- const HELP = `Usage: oneclickpush [options]
6
+ const DEFAULTS = {
7
+ exclude: ['**/.DS_Store'],
8
+ branch: 'gh-pages',
9
+ message: 'Updates',
10
+ remote: 'origin',
11
+ force: false,
12
+ dryRun: false,
13
+ logLevel: 'normal'
14
+ }
15
+
16
+ const HELP = `Usage: oneclickpush <dir> [options]
7
17
 
8
18
  Replace the contents of a remote git branch with that of a
9
19
  local directory.
10
20
 
21
+ Arguments:
22
+ dir Directory to deploy (required)
23
+
11
24
  Options:
12
- -d, --dist <dir> Directory to publish (default: ".")
13
- -e, --exclude <glob> Paths to leave out, relative to dist
14
- (default: "**/.DS_Store")
15
- -b, --branch <name> Target branch (default: "gh-pages")
16
- -m, --message <msg> Commit message (default: "Updates")
17
- -r, --remote <name|url> Remote name or repo URL to publish to
18
- (default: "origin")
25
+ -e, --exclude <glob> Paths to leave out, relative to dir
26
+ (default: "${DEFAULTS.exclude.join(', ')}")
27
+ -b, --branch <name> Target branch (default: "${DEFAULTS.branch}")
28
+ -m, --message <msg> Commit message (default: "${DEFAULTS.message}")
29
+ -r, --remote <name|url> Remote name or repo URL to deploy to
30
+ (default: "${DEFAULTS.remote}")
19
31
  -a, --author <author> Commit author, as "Name <email>"
20
32
  -f, --force Force push new commit with no history
21
33
  -n, --dry-run Show what would change, without
22
34
  committing or pushing
23
- -v, --verbose Log git commands
35
+ -l, --log-level <level> Output: silent, normal or verbose
36
+ (default: "${DEFAULTS.logLevel}")
24
37
  -V, --version Print version
25
38
  -h, --help Show this help
26
39
 
27
40
  --exclude takes glob patterns (*, **, ?) and can be repeated.
28
41
  Patterns are matched against the full path, so use **/name to
29
- match at any depth.`
42
+ match at any depth.
43
+
44
+ Log levels: "silent" prints nothing (check the exit code),
45
+ "normal" prints the result and errors, "verbose" also logs git
46
+ commands and full errors.`
30
47
 
31
- const { values: v } = parseArgs({
32
- options: {
33
- dist: { type: 'string', short: 'd', default: '.' },
34
- exclude: { type: 'string', short: 'e', multiple: true, default: ['**/.DS_Store'] },
35
- branch: { type: 'string', short: 'b', default: 'gh-pages' },
36
- message: { type: 'string', short: 'm', default: 'Updates' },
37
- remote: { type: 'string', short: 'r', default: 'origin' },
38
- author: { type: 'string', short: 'a' },
39
- force: { type: 'boolean', short: 'f', default: false },
40
- 'dry-run': { type: 'boolean', short: 'n', default: false },
41
- verbose: { type: 'boolean', short: 'v', default: false },
42
- version: { type: 'boolean', short: 'V', default: false },
43
- help: { type: 'boolean', short: 'h', default: false }
48
+ const LEVELS = ['silent', 'normal', 'verbose']
49
+
50
+ const { values: v, positionals } = (() => {
51
+ try {
52
+ return parseArgs({
53
+ allowPositionals: true,
54
+ options: {
55
+ exclude: { type: 'string', short: 'e', multiple: true, default: DEFAULTS.exclude },
56
+ branch: { type: 'string', short: 'b', default: DEFAULTS.branch },
57
+ message: { type: 'string', short: 'm', default: DEFAULTS.message },
58
+ remote: { type: 'string', short: 'r', default: DEFAULTS.remote },
59
+ author: { type: 'string', short: 'a' },
60
+ force: { type: 'boolean', short: 'f', default: DEFAULTS.force },
61
+ 'dry-run': { type: 'boolean', short: 'n', default: DEFAULTS.dryRun },
62
+ 'log-level': { type: 'string', short: 'l', default: DEFAULTS.logLevel },
63
+ version: { type: 'boolean', short: 'V', default: false },
64
+ help: { type: 'boolean', short: 'h', default: false }
65
+ }
66
+ })
67
+ } catch (/** @type {any} */ e) {
68
+ if (!e.code?.startsWith('ERR_PARSE_ARGS_')) throw e
69
+ // Keep only the first sentence, Node appends a long "-- " hint to some messages
70
+ const msg = e.message.split('. ')[0].replace(/\.$/, '')
71
+ console.error(`${msg}, run "oneclickpush --help" for usage`)
72
+ process.exit(1)
44
73
  }
45
- })
74
+ })()
46
75
 
47
76
  if (v.help) {
48
77
  console.log(HELP)
@@ -55,27 +84,46 @@ if (v.version) {
55
84
  process.exit(0)
56
85
  }
57
86
 
58
- // Dead simple spinner (skipped when verbose or piped)
87
+ if (positionals.length !== 1) {
88
+ console.error(
89
+ positionals.length === 0
90
+ ? 'Missing required argument <dir>, run "oneclickpush --help" for usage'
91
+ : 'Expected exactly one directory, run "oneclickpush --help" for usage'
92
+ )
93
+ process.exit(1)
94
+ }
95
+ const dir = positionals[0]
96
+
97
+ const level = v['log-level']
98
+ if (!LEVELS.includes(level)) {
99
+ console.error(`Invalid --log-level "${level}", expected one of: ${LEVELS.join(', ')}`)
100
+ process.exit(1)
101
+ }
102
+
103
+ // Dead simple spinner (normal level, TTY only)
59
104
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
60
105
  let i = 0
61
106
  const spin =
62
- process.stderr.isTTY && !v.verbose
107
+ process.stderr.isTTY && level === 'normal'
63
108
  ? setInterval(() => process.stderr.write(`\r${frames[i++ % frames.length]}`), 100)
64
109
  : null
65
110
  const stop = () => spin && (clearInterval(spin), process.stderr.write('\r \r'))
66
111
 
67
112
  try {
68
- const changes = await publish({ ...v, dryRun: v['dry-run'] })
113
+ const changes = await deploy({ ...v, dir, verbose: level === 'verbose', dryRun: v['dry-run'] })
69
114
  stop()
70
- console.log(
71
- !changes
72
- ? 'Nothing to publish'
73
- : v['dry-run']
74
- ? `Dry run, would publish:\n${changes}`
75
- : 'Published'
76
- )
115
+ if (level !== 'silent') {
116
+ console.log(
117
+ !changes
118
+ ? 'Nothing to deploy'
119
+ : v['dry-run']
120
+ ? `Dry run, would deploy:\n${changes}`
121
+ : 'Deployed!'
122
+ )
123
+ }
77
124
  } catch (/** @type {any} */ e) {
78
125
  stop()
79
- console.error(v.verbose ? e : e.message)
126
+ if (level === 'verbose') console.error(e)
127
+ else if (level === 'normal') console.error(e.message)
80
128
  process.exit(1)
81
129
  }
package/index.d.ts CHANGED
@@ -1,61 +1,20 @@
1
- export type OneclickpushOptions = {
2
- /**
3
- * Directory to publish
4
- */
5
- dist?: string
6
- /**
7
- * Paths to leave out, relative to dist (default: `**\/.DS_Store`)
8
- */
9
- exclude?: string[]
10
- /**
11
- * Target branch
12
- */
13
- branch?: string
14
- /**
15
- * Commit message
16
- */
17
- message?: string
18
- /**
19
- * Remote name of the current repo, or a repository URL or path
20
- */
21
- remote?: string
22
- /**
23
- * Commit author and committer as "Name <email>"
24
- */
25
- author?: string
26
- /**
27
- * Force push new commit with no history
28
- */
29
- force?: boolean
30
- /**
31
- * Show what would change, without committing or pushing
32
- */
33
- dryRun?: boolean
34
- /**
35
- * Log git commands
36
- */
37
- verbose?: boolean
38
- }
39
- /**
40
- * @typedef {object} OneclickpushOptions
41
- * @prop {string} [dist="."] Directory to publish
42
- * @prop {string[]} [exclude] Paths to leave out, relative to dist (default: `**\/.DS_Store`)
43
- * @prop {string} [branch="gh-pages"] Target branch
44
- * @prop {string} [message="Updates"] Commit message
45
- * @prop {string} [remote="origin"] Remote name of the current repo, or a repository URL or path
46
- * @prop {string} [author] Commit author and committer as "Name <email>"
47
- * @prop {boolean} [force=false] Force push new commit with no history
48
- * @prop {boolean} [dryRun=false] Show what would change, without committing or pushing
49
- * @prop {boolean} [verbose=false] Log git commands
50
- */
51
1
  /**
52
2
  * Replace the contents of a git branch with the contents of a directory.
53
3
  * Returns the staged changes as `git diff --name-status` text or "" if nothing changed.
54
- * @param {OneclickpushOptions} options
4
+ * @param {object} o
5
+ * @param {string} o.dir Directory to deploy (required)
6
+ * @param {string[]} [o.exclude] Paths to leave out, relative to dir (default: `**\/.DS_Store`)
7
+ * @param {string} [o.branch="gh-pages"] Target branch
8
+ * @param {string} [o.message="Updates"] Commit message
9
+ * @param {string} [o.remote="origin"] Remote name of the current repo, or a repository URL or path
10
+ * @param {string} [o.author] Commit author and committer as "Name <email>"
11
+ * @param {boolean} [o.force=false] Force push new commit with no history
12
+ * @param {boolean} [o.dryRun=false] Return staged changes without committing or pushing
13
+ * @param {boolean} [o.verbose=false] Log git commands to stderr
55
14
  * @returns {Promise<string>}
56
15
  */
57
- export declare function publish({
58
- dist,
16
+ export declare function deploy({
17
+ dir,
59
18
  exclude,
60
19
  branch,
61
20
  message,
@@ -64,4 +23,14 @@ export declare function publish({
64
23
  force,
65
24
  dryRun,
66
25
  verbose
67
- }: OneclickpushOptions): Promise<string>
26
+ }: {
27
+ dir: string
28
+ exclude?: string[]
29
+ branch?: string
30
+ message?: string
31
+ remote?: string
32
+ author?: string
33
+ force?: boolean
34
+ dryRun?: boolean
35
+ verbose?: boolean
36
+ }): Promise<string>
package/index.js CHANGED
@@ -4,27 +4,23 @@ import { mkdtemp, rm } from 'node:fs/promises'
4
4
  import { tmpdir } from 'node:os'
5
5
  import { join, resolve } from 'node:path'
6
6
 
7
- /**
8
- * @typedef {object} OneclickpushOptions
9
- * @prop {string} [dist="."] Directory to publish
10
- * @prop {string[]} [exclude] Paths to leave out, relative to dist (default: `**\/.DS_Store`)
11
- * @prop {string} [branch="gh-pages"] Target branch
12
- * @prop {string} [message="Updates"] Commit message
13
- * @prop {string} [remote="origin"] Remote name of the current repo, or a repository URL or path
14
- * @prop {string} [author] Commit author and committer as "Name <email>"
15
- * @prop {boolean} [force=false] Force push new commit with no history
16
- * @prop {boolean} [dryRun=false] Show what would change, without committing or pushing
17
- * @prop {boolean} [verbose=false] Log git commands
18
- */
19
-
20
7
  /**
21
8
  * Replace the contents of a git branch with the contents of a directory.
22
9
  * Returns the staged changes as `git diff --name-status` text or "" if nothing changed.
23
- * @param {OneclickpushOptions} options
10
+ * @param {object} o
11
+ * @param {string} o.dir Directory to deploy (required)
12
+ * @param {string[]} [o.exclude] Paths to leave out, relative to dir (default: `**\/.DS_Store`)
13
+ * @param {string} [o.branch="gh-pages"] Target branch
14
+ * @param {string} [o.message="Updates"] Commit message
15
+ * @param {string} [o.remote="origin"] Remote name of the current repo, or a repository URL or path
16
+ * @param {string} [o.author] Commit author and committer as "Name <email>"
17
+ * @param {boolean} [o.force=false] Force push new commit with no history
18
+ * @param {boolean} [o.dryRun=false] Return staged changes without committing or pushing
19
+ * @param {boolean} [o.verbose=false] Log git commands to stderr
24
20
  * @returns {Promise<string>}
25
21
  */
26
- export async function publish({
27
- dist = '.',
22
+ export async function deploy({
23
+ dir,
28
24
  exclude = ['**/.DS_Store'],
29
25
  branch = 'gh-pages',
30
26
  message = 'Updates',
@@ -34,7 +30,8 @@ export async function publish({
34
30
  dryRun = false,
35
31
  verbose = false
36
32
  }) {
37
- dist = resolve(dist)
33
+ if (!dir) throw new Error('dir is required')
34
+ dir = resolve(dir)
38
35
  const run = promisify(execFile)
39
36
 
40
37
  // Env vars beat git config, so this also overrides any identity already set in CI
@@ -54,7 +51,8 @@ export async function publish({
54
51
  /** @type {(args: string[], cwd?: string, env?: Record<string, string>) => Promise<string>} */
55
52
  const git = async (args, cwd, env) => {
56
53
  if (verbose) console.error(`$ git ${args.join(' ')}`)
57
- return (await run('git', args, { cwd, env: { ...process.env, ...env } })).stdout.trim()
54
+ const { stdout } = await run('git', args, { cwd, env: { ...process.env, ...env } })
55
+ return stdout.trim()
58
56
  }
59
57
 
60
58
  // A remote name resolves to its url; anything else is used as a url or path as-is
@@ -82,21 +80,21 @@ export async function publish({
82
80
  await git(['remote', 'add', 'origin', url], tmp)
83
81
  }
84
82
 
85
- // Empty the index, then stage everything from dist except excluded paths
83
+ // Empty the index, then stage everything from dir except excluded paths
86
84
  await git(['read-tree', '--empty'], tmp)
87
85
  await git(
88
86
  [
89
87
  '--git-dir',
90
88
  join(tmp, '.git'),
91
89
  '--work-tree',
92
- dist,
90
+ dir,
93
91
  'add',
94
92
  '--force',
95
93
  '--',
96
94
  '.',
97
95
  ...exclude.map((p) => `:(exclude,glob)${p}`)
98
96
  ],
99
- dist
97
+ dir
100
98
  )
101
99
 
102
100
  const status = await git(['diff', '--cached', '--name-status'], tmp)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oneclickpush",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "One-click GitHub Pages deployer",
5
5
  "author": "Jason Lee <jason@zerodevx.com>",
6
6
  "scripts": {
@@ -33,6 +33,7 @@
33
33
  "keywords": [
34
34
  "one-click",
35
35
  "gh-pages",
36
- "deploy"
36
+ "deploy",
37
+ "cli"
37
38
  ]
38
39
  }