@ajaykumarnpm/talea 0.5.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All notable changes to this project are recorded here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the versions follow
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.7.0] - 2026-09-22
8
+
9
+ ### Added
10
+ - a bare repo name narrows sync, clone, status, list and tree
11
+
12
+ ## [0.6.0] - 2026-09-22
13
+
14
+ ### Breaking
15
+ - `talea pick <repo>` keeps one repo; `talea update` upgrades the CLI
16
+
7
17
  ## [0.5.0] - 2026-09-22
8
18
 
9
19
  ### Added
package/README.md CHANGED
@@ -66,6 +66,7 @@ talea sync # clone the new, fast-forward the rest
66
66
  talea status # branch, clean/dirty, ahead/behind, in one table
67
67
  talea add some-repo # keep one more on this machine, and clone it now
68
68
  talea select # reopen the checklist and change the whole list
69
+ talea pick some-repo # keep that one — an unknown name opens the checklist
69
70
  cd $(talea where eklavya)
70
71
  ```
71
72
 
@@ -167,7 +168,7 @@ Treat the id like a bookmark you would not paste into a public channel.
167
168
  | `talea clone` | clone only — never fetches or merges |
168
169
  | `talea adopt` | move checkouts you already have into place |
169
170
  | `talea status` | branch, clean/dirty, ahead/behind |
170
- | `talea select` | reopen the checklist — what this machine keeps |
171
+ | `talea select` | reopen the checklist — `talea pick <repo>` for one |
171
172
  | `talea add` / `talea rm` | change that one repo at a time |
172
173
  | `talea where <repo>` | print a repo's path, for `cd $( )` |
173
174
  | `talea list` | the catalogue |
@@ -176,10 +177,12 @@ Treat the id like a bookmark you would not paste into a public channel.
176
177
  | `talea manifest push/pull` | move the catalogue between machines |
177
178
  | `talea skill` | install the skill that lets your coding agent drive talea |
178
179
  | `talea doctor` | check this machine can do the work |
179
- | `talea upgrade` | update the CLI itself |
180
+ | `talea upgrade` | update the CLI itself (also `talea update`) |
180
181
 
181
182
  Every one of them takes `-g <group>` and `-r <repo>` to narrow the run, and
182
- `--help` for its own examples.
183
+ `--help` for its own examples. For `sync`, `clone`, `status`, `list` and `tree`
184
+ a bare name means the same as `-r`, so `talea sync eklavya` syncs that one repo.
185
+ A command that takes no names refuses a stray word instead of ignoring it.
183
186
 
184
187
  ---
185
188
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ajaykumarnpm/talea",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "One folder structure for every machine. Clone, adopt and sync your GitHub repos from a manifest you own.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -74,7 +74,7 @@ skipped, and saying so up front is better than reporting it afterwards.
74
74
  talea sync # clone what is missing, fast-forward what is there
75
75
  talea clone # clone only — never fetches or merges
76
76
  talea sync -g NonStop # one group
77
- talea sync -r eklavya # one repo
77
+ talea sync eklavya # one repo — same as -r eklavya
78
78
  ```
79
79
 
80
80
  `sync` is safe to run unattended. It fast-forwards **the branch you are on** and
@@ -117,11 +117,17 @@ and nothing in talea will do it for them.
117
117
  talea add some-repo # keep one more here, and clone it now
118
118
  talea rm some-repo # stop keeping it — the checkout stays exactly where it is
119
119
  talea select # reopen the whole checklist, interactively
120
+ talea pick some-repo # = add when the name is exact; otherwise the checklist
120
121
  ```
121
122
 
122
123
  `talea select` needs a terminal. In a non-interactive session it prints a
123
124
  summary instead of hanging, so prefer `add` and `rm` when acting on the
124
125
  developer's behalf and leave `select` as something to suggest they run.
126
+ `pick <name>` falls back to that same checklist on a typo, so an agent uses
127
+ `add`, which fails loudly instead.
128
+
129
+ `talea update` reinstalls the CLI (it is `upgrade`), not `sync`. Never run it
130
+ to refresh repos.
125
131
 
126
132
  `rm` does **not** delete the checkout. Say that plainly when you run it, or it
127
133
  reads like data loss.
@@ -156,8 +162,11 @@ cloned — `talea init` or `talea select` after it is what fills the tree.
156
162
  ## Narrowing any run
157
163
 
158
164
  Every command takes `-g <group>` and `-r <repo>`, both repeatable, and `--help`
159
- for its own examples. An unknown group or repo name **exits non-zero** rather
160
- than quietly doing nothing, so a typo is loud.
165
+ for its own examples. For `sync`, `clone`, `status`, `list` and `tree` a bare
166
+ name is the same as `-r`. `adopt` is the exception: it refuses a bare name,
167
+ because `-r` there lifts the name-only guard and must be typed on purpose. An
168
+ unknown group or repo name **exits non-zero** rather than quietly doing nothing,
169
+ so a typo is loud.
161
170
 
162
171
  ## Rules
163
172
 
package/src/cli.js CHANGED
@@ -52,7 +52,6 @@ const ALIASES = {
52
52
  setup: 'init',
53
53
  bootstrap: 'init',
54
54
  pull: 'sync',
55
- update: 'sync',
56
55
  refresh: 'discover',
57
56
  pick: 'select',
58
57
  choose: 'select',
@@ -63,9 +62,28 @@ const ALIASES = {
63
62
  run: 'exec',
64
63
  skills: 'skill',
65
64
  remove: 'rm',
65
+ update: 'upgrade',
66
66
  'self-update': 'upgrade',
67
67
  };
68
68
 
69
+ // Bare words after these commands are repo names: `talea sync PiDom` is
70
+ // `talea sync -r PiDom`. Dropping them instead ran the command over every repo
71
+ // on the machine, which is a typo guard failing in the worst direction.
72
+ const TAKES_REPOS = new Set(['sync', 'clone', 'status', 'list', 'tree']);
73
+
74
+ // These read their own positionals. Everything else takes none, and a stray
75
+ // word stops the run. `adopt` is left out on purpose: an explicit -r there lifts
76
+ // the name-only guard, and a bare word must not do that by accident.
77
+ const TAKES_WORDS = new Set(['init', 'select', 'add', 'rm', 'manifest', 'exec', 'skill', 'where']);
78
+
79
+ /** Fold a command's bare words into -r, pass them through, or refuse them. */
80
+ export function routeWords(key, words, repo) {
81
+ if (!words.length || TAKES_WORDS.has(key)) return { repo, words };
82
+ if (TAKES_REPOS.has(key)) return { repo: [...(repo ?? []), ...words], words: [] };
83
+ const hint = key === 'adopt' ? `\n To name a repo: talea adopt -r ${words.join(',')}` : '';
84
+ return { error: `\`${key}\` takes no arguments, got ${words.map((w) => `"${w}"`).join(' ')}.${hint}` };
85
+ }
86
+
69
87
  const OPTIONS = {
70
88
  group: { type: 'string', short: 'g', multiple: true },
71
89
  repo: { type: 'string', short: 'r', multiple: true },
@@ -110,7 +128,7 @@ ${c.bold('Commands')}
110
128
  ${c.cyan('clone')} clone only — never fetches or merges
111
129
  ${c.cyan('adopt')} move repos you already have into the right place
112
130
  ${c.cyan('status')} one table: branch, clean/dirty, ahead/behind
113
- ${c.cyan('select')} reopen the checklist — what this machine keeps
131
+ ${c.cyan('select')} reopen the checklist — or ${c.dim('talea pick <repo>')} for one
114
132
  ${c.cyan('add')} keep another repo on this machine (${c.dim('rm')} to drop one)
115
133
  ${c.cyan('where')} print a repo's path — ${c.dim('cd $(talea where eklavya)')}
116
134
  ${c.cyan('list')} show the catalogue
@@ -203,6 +221,12 @@ export async function main(argv) {
203
221
  }
204
222
  }
205
223
 
224
+ const routed = routeWords(key, rest, values.repo?.length ? values.repo : undefined);
225
+ if (routed.error) {
226
+ fail(routed.error);
227
+ process.exit(1);
228
+ }
229
+
206
230
  const opts = {
207
231
  ...values,
208
232
  jobs,
@@ -216,10 +240,10 @@ export async function main(argv) {
216
240
  // selectRepos to flatten, but normalise "not passed" to undefined.
217
241
  group: values.group?.length ? values.group : undefined,
218
242
  from: values.from?.length ? values.from : undefined,
219
- repo: values.repo?.length ? values.repo : undefined,
243
+ repo: routed.repo,
220
244
  };
221
245
 
222
- await command.run(opts, key === 'exec' ? tail : rest);
246
+ await command.run(opts, key === 'exec' ? tail : routed.words);
223
247
 
224
248
  // After the real work, never before it, and never able to fail it.
225
249
  if (key !== 'upgrade') {
@@ -34,12 +34,17 @@ Options
34
34
  -j, --jobs <n> parallel clones
35
35
  `;
36
36
 
37
- /** Resolve `name` or `owner/name` against the catalogue, or exit saying why. */
38
- export function resolve(manifest, name) {
37
+ /** Every catalogue entry `name` or `owner/name` could mean, ignoring case. */
38
+ export function lookup(manifest, name) {
39
39
  const wanted = String(name).toLowerCase();
40
- const matches = manifest.repos.filter(
40
+ return manifest.repos.filter(
41
41
  (r) => r.name.toLowerCase() === wanted || `${r.owner}/${r.name}`.toLowerCase() === wanted,
42
42
  );
43
+ }
44
+
45
+ /** Resolve `name` or `owner/name` against the catalogue, or exit saying why. */
46
+ export function resolve(manifest, name) {
47
+ const matches = lookup(manifest, name);
43
48
 
44
49
  if (!matches.length) {
45
50
  fail(`No repo called "${name}" in the catalogue.`);
@@ -20,7 +20,7 @@ ${c.bold('talea clone')} — clone what is missing, and nothing else
20
20
 
21
21
  Options
22
22
  -g, --group <names> comma-separated groups
23
- -r, --repo <names> comma-separated repo names
23
+ -r, --repo <names> comma-separated repo names; a bare name works too
24
24
  --pick choose what this machine keeps before cloning
25
25
  --protocol <p> ssh (default) or https
26
26
  --from <path> also search here for existing checkouts (repeatable)
@@ -15,7 +15,7 @@ the catalogue's opinion — what a brand new machine would start with.
15
15
 
16
16
  Options
17
17
  -g, --group <names> comma-separated groups
18
- -r, --repo <names> comma-separated repo names
18
+ -r, --repo <names> comma-separated repo names; a bare name works too
19
19
  --all include archived and quiet repos (default: hidden)
20
20
  --groups show groups only
21
21
  --json emit JSON
@@ -9,9 +9,10 @@
9
9
  // because `clone` and `sync` ask the same question, and the cloning is
10
10
  // `clone.run`, re-entered after the new selection is on disk.
11
11
 
12
- import { c, fail, heading, ok, plain, skip } from '../log.js';
12
+ import { c, fail, heading, ok, plain, skip, warn } from '../log.js';
13
13
  import { chooseRepos } from '../select.js';
14
14
  import { machineRepos, requireCatalogue, requireWorkspace } from '../workspace.js';
15
+ import * as add from './add.js';
15
16
  import * as clone from './clone.js';
16
17
 
17
18
  export const help = `
@@ -19,6 +20,8 @@ ${c.bold('talea select')} — change what this machine keeps
19
20
 
20
21
  ${c.dim('talea select')} reopen the checklist, then clone what is newly ticked
21
22
  ${c.dim('talea select --no-clone')} change the list only, fill it in later
23
+ ${c.dim('talea pick PiDom')} keep that one repo and clone it — or, if no repo
24
+ has that name, open the checklist instead
22
25
 
23
26
  The checklist opens with the current selection ticked. Space toggles, Enter
24
27
  saves, Esc cancels and changes nothing. Everything in the catalogue is listed,
@@ -29,7 +32,10 @@ It writes ${c.dim('.talea.json')}, which never leaves this machine, so no other
29
32
  selection changes. Unticking a repo takes it off the list and leaves the
30
33
  checkout exactly where it is; deleting it is your call.
31
34
 
32
- For one repo, ${c.dim('talea add <repo>')} and ${c.dim('talea rm <repo>')} skip the checklist.
35
+ A name that matches exactly one repo (case does not matter, ${c.dim('owner/name')} works)
36
+ is ${c.dim('talea add <repo>')}. A name that matches none, or two owners' repos, opens the
37
+ checklist, because a typo should land you in the list rather than nowhere.
38
+ ${c.dim('talea rm <repo>')} takes one off.
33
39
 
34
40
  Options
35
41
  --no-clone save the selection, clone nothing
@@ -37,6 +43,11 @@ Options
37
43
  -j, --jobs <n> parallel clones
38
44
  `;
39
45
 
46
+ /** The names that do not pin down exactly one repo — empty means every one does. */
47
+ export function unresolved(manifest, names) {
48
+ return names.filter((n) => add.lookup(manifest, n).length !== 1);
49
+ }
50
+
40
51
  /** What changed, as two lists of names. Pure, so it can be tested. */
41
52
  export function changes(before, after) {
42
53
  const had = new Set(before);
@@ -47,7 +58,7 @@ export function changes(before, after) {
47
58
  };
48
59
  }
49
60
 
50
- export async function run(opts) {
61
+ export async function run(opts, positionals = []) {
51
62
  const { root, manifest, state } = await requireWorkspace();
52
63
  requireCatalogue(manifest);
53
64
 
@@ -60,6 +71,12 @@ export async function run(opts) {
60
71
  process.exit(1);
61
72
  }
62
73
 
74
+ if (positionals.length) {
75
+ const missing = unresolved(manifest, positionals);
76
+ if (!missing.length) return add.run({ ...opts, removing: false }, positionals);
77
+ warn(`No single repo called ${missing.map((n) => `"${n}"`).join(', ')} — opening the checklist.`);
78
+ }
79
+
63
80
  const before = machineRepos(manifest, state).map((r) => r.name);
64
81
  const { repos } = await chooseRepos({ manifest, root, state, opts: { ...opts, pick: true } });
65
82
  const after = repos.map((r) => r.name);
@@ -22,7 +22,7 @@ Columns
22
22
 
23
23
  Options
24
24
  -g, --group <names> comma-separated groups
25
- -r, --repo <names> comma-separated repo names
25
+ -r, --repo <names> comma-separated repo names; a bare name works too
26
26
  --drift only repos on some other branch
27
27
  --missing only repos not cloned yet
28
28
  --all ignore this machine's selection
@@ -23,6 +23,7 @@ ${c.bold('talea sync')} — make this machine match the list
23
23
 
24
24
  ${c.dim('talea sync')} clone what is missing, fast-forward the rest
25
25
  ${c.dim('talea sync --pick')} change what this machine keeps, then sync
26
+ ${c.dim('talea sync eklavya')} only that repo (same as -r eklavya)
26
27
  ${c.dim('talea sync -g nonstopio')} only that owner
27
28
  ${c.dim('talea sync --no-clone')} fast-forward only, clone nothing
28
29
 
@@ -36,7 +37,7 @@ the workspace ${c.dim('talea init')} made, or asks which when this machine has s
36
37
 
37
38
  Options
38
39
  -g, --group <names> comma-separated groups
39
- -r, --repo <names> comma-separated repo names
40
+ -r, --repo <names> comma-separated repo names; a bare name works too
40
41
  --pick re-open the checklist before syncing
41
42
  --no-clone do not clone anything new
42
43
  --no-adopt do not look for checkouts to move into place
@@ -24,7 +24,7 @@ their docs come from the CLI — run \`talea sync\` to drop in any that are new.
24
24
 
25
25
  Options
26
26
  -g, --group <names> comma-separated groups
27
- -r, --repo <names> comma-separated repo names
27
+ -r, --repo <names> comma-separated repo names; a bare name works too
28
28
  --all the whole catalogue, not just what this machine keeps
29
29
  `;
30
30
 
@@ -6,6 +6,7 @@ export const help = `
6
6
  ${c.bold('talea upgrade')} — update the CLI itself
7
7
 
8
8
  ${c.dim('talea upgrade')} reinstall from npm at the latest version
9
+ ${c.dim('talea update')} the same thing
9
10
  ${c.dim('talea upgrade --check')} only report whether one is available
10
11
  ${c.dim('talea upgrade --on')} turn the daily update notice on
11
12
  ${c.dim('talea upgrade --off')} turn the daily update notice off
package/src/git.js CHANGED
@@ -181,7 +181,11 @@ export async function currentBranch(dir) {
181
181
  const { code, stdout } = await git(['rev-parse', '--abbrev-ref', 'HEAD'], {
182
182
  cwd: dir,
183
183
  });
184
- return code === 0 ? stdout : null;
184
+ if (code === 0) return stdout;
185
+ // A repo with no commits yet has no HEAD to resolve, so rev-parse fails and
186
+ // the caller printed "null". symbolic-ref still knows the branch's name.
187
+ const born = await git(['symbolic-ref', '--short', 'HEAD'], { cwd: dir });
188
+ return born.code === 0 ? born.stdout : null;
185
189
  }
186
190
 
187
191
  /** True when the working tree has uncommitted changes (tracked or untracked). */