@ajaykumarnpm/talea 0.2.0 → 0.3.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 ADDED
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the versions follow
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.3.0] - 2026-09-21
8
+
9
+ ### Added
10
+ - talea select reopens the checklist
11
+
12
+ ### Chores
13
+ - ignore .idea/
14
+
15
+ ## [0.2.2] - 2026-09-21
16
+
17
+ ### Documentation
18
+ - the skip marker is read from the commit body too
19
+
20
+ ### Chores
21
+ - publish before pushing the tag
22
+ - the pipeline cuts the release, not a laptop
23
+
24
+ ## [0.2.1] - 2026-09-21
25
+
26
+ ### Fixed
27
+ - The live block no longer repeats itself down the screen. A row wider than the
28
+ terminal wrapped onto a second line, which threw off the cursor arithmetic the
29
+ block redraws with, so every frame landed a line lower than the last. Rows are
30
+ now cut to the window width.
31
+
32
+ ### Chores
33
+ - Releases are published by a GitHub release rather than by pushing a tag, and
34
+ npm authenticates with trusted publishing instead of a stored token.
35
+
36
+ ## [0.2.0] - 2026-09-21
37
+
38
+ ### Added
39
+ - `ignore: true` in the catalogue marks a checkout that another tool owns. No
40
+ command touches it — not `sync`, not `adopt`, not an explicit selection.
41
+
42
+ ### Fixed
43
+ - `init` defaults to `~/Workspace` and refuses to turn your home directory into
44
+ a workspace.
45
+ - `adopt` moves a repo's worktrees with it and repairs their links, instead of
46
+ leaving them stranded.
47
+ - A repo that matches only by name is never moved unattended. Say `--loose`, or
48
+ name it with `-r`, to move one anyway.
49
+
50
+ ## [0.1.0] - 2026-09-21
51
+
52
+ ### Added
53
+ - First published release: `discover`, `clone`, `sync`, `adopt`, `add`, `rm`,
54
+ `where`, `status` and `doctor`, the live progress block, and the shared
55
+ catalogue with a per-machine selection that never travels with it.
56
+ - Published as `@ajaykumarnpm/talea` — npm's typo-squatting filter refused the
57
+ bare name.
package/README.md CHANGED
@@ -60,6 +60,7 @@ this machine needs, untick what it does not, press enter.
60
60
  talea sync # clone the new, fast-forward the rest
61
61
  talea status # branch, clean/dirty, ahead/behind, in one table
62
62
  talea add some-repo # keep one more on this machine, and clone it now
63
+ talea select # reopen the checklist and change the whole list
63
64
  cd $(talea where eklavya)
64
65
  ```
65
66
 
@@ -146,7 +147,8 @@ Treat the id like a bookmark you would not paste into a public channel.
146
147
  | `talea clone` | clone only — never fetches or merges |
147
148
  | `talea adopt` | move checkouts you already have into place |
148
149
  | `talea status` | branch, clean/dirty, ahead/behind |
149
- | `talea add` / `talea rm` | change what this machine keeps |
150
+ | `talea select` | reopen the checklist — what this machine keeps |
151
+ | `talea add` / `talea rm` | change that one repo at a time |
150
152
  | `talea where <repo>` | print a repo's path, for `cd $( )` |
151
153
  | `talea list` | the catalogue |
152
154
  | `talea tree` | the folder tree on disk |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ajaykumarnpm/talea",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": {
@@ -17,7 +17,8 @@
17
17
  "src",
18
18
  "manifest",
19
19
  "templates",
20
- "README.md"
20
+ "README.md",
21
+ "CHANGELOG.md"
21
22
  ],
22
23
  "scripts": {
23
24
  "test": "node --test"
package/src/cli.js CHANGED
@@ -8,6 +8,7 @@ import { c, fail, plain } from './log.js';
8
8
  import * as init from './commands/init.js';
9
9
  import * as discover from './commands/discover.js';
10
10
  import * as clone from './commands/clone.js';
11
+ import * as select from './commands/select.js';
11
12
  import * as adopt from './commands/adopt.js';
12
13
  import * as sync from './commands/sync.js';
13
14
  import * as status from './commands/status.js';
@@ -28,6 +29,7 @@ const COMMANDS = {
28
29
  init,
29
30
  discover,
30
31
  clone,
32
+ select,
31
33
  adopt,
32
34
  sync,
33
35
  status,
@@ -50,6 +52,8 @@ const ALIASES = {
50
52
  pull: 'sync',
51
53
  update: 'sync',
52
54
  refresh: 'discover',
55
+ pick: 'select',
56
+ choose: 'select',
53
57
  ls: 'list',
54
58
  st: 'status',
55
59
  cd: 'where',
@@ -103,6 +107,7 @@ ${c.bold('Commands')}
103
107
  ${c.cyan('clone')} clone only — never fetches or merges
104
108
  ${c.cyan('adopt')} move repos you already have into the right place
105
109
  ${c.cyan('status')} one table: branch, clean/dirty, ahead/behind
110
+ ${c.cyan('select')} reopen the checklist — what this machine keeps
106
111
  ${c.cyan('add')} keep another repo on this machine (${c.dim('rm')} to drop one)
107
112
  ${c.cyan('where')} print a repo's path — ${c.dim('cd $(talea where eklavya)')}
108
113
  ${c.cyan('list')} show the catalogue
@@ -0,0 +1,79 @@
1
+ // `talea select` — reopen the checklist and change what this machine keeps.
2
+ //
3
+ // `add` and `rm` are the one-repo door. This is the door for "let me look at
4
+ // the whole list again", which until now was reachable only as `--pick` on a
5
+ // command you had to already know to run. The picker was the first thing `init`
6
+ // showed and then had no name of its own.
7
+ //
8
+ // It is deliberately thin: the picking and the saving live in `src/select.js`
9
+ // because `clone` and `sync` ask the same question, and the cloning is
10
+ // `clone.run`, re-entered after the new selection is on disk.
11
+
12
+ import { c, fail, heading, ok, plain, skip } from '../log.js';
13
+ import { chooseRepos } from '../select.js';
14
+ import { machineRepos, requireCatalogue, requireWorkspace } from '../workspace.js';
15
+ import * as clone from './clone.js';
16
+
17
+ export const help = `
18
+ ${c.bold('talea select')} — change what this machine keeps
19
+
20
+ ${c.dim('talea select')} reopen the checklist, then clone what is newly ticked
21
+ ${c.dim('talea select --no-clone')} change the list only, fill it in later
22
+
23
+ The checklist opens with the current selection ticked. Space toggles, Enter
24
+ saves, Esc cancels and changes nothing. Everything in the catalogue is listed,
25
+ including the archived and the forks — this machine is allowed to keep
26
+ something the default set does not have.
27
+
28
+ It writes ${c.dim('.talea.json')}, which never leaves this machine, so no other machine's
29
+ selection changes. Unticking a repo takes it off the list and leaves the
30
+ checkout exactly where it is; deleting it is your call.
31
+
32
+ For one repo, ${c.dim('talea add <repo>')} and ${c.dim('talea rm <repo>')} skip the checklist.
33
+
34
+ Options
35
+ --no-clone save the selection, clone nothing
36
+ --protocol <p> ssh (default) or https
37
+ -j, --jobs <n> parallel clones
38
+ `;
39
+
40
+ /** What changed, as two lists of names. Pure, so it can be tested. */
41
+ export function changes(before, after) {
42
+ const had = new Set(before);
43
+ const has = new Set(after);
44
+ return {
45
+ added: after.filter((n) => !had.has(n)),
46
+ dropped: before.filter((n) => !has.has(n)),
47
+ };
48
+ }
49
+
50
+ export async function run(opts) {
51
+ const { root, manifest, state } = requireWorkspace();
52
+ requireCatalogue(manifest);
53
+
54
+ // -g/-r narrow a run; they cannot narrow a decision about the whole machine.
55
+ // Accepting them silently would return the old selection unchanged and look
56
+ // like the picker had simply declined to open.
57
+ if (opts.group || opts.repo) {
58
+ fail('`select` is about the whole list — -g/-r do not narrow it.');
59
+ console.error('\n Use `talea add <repo>` or `talea rm <repo>` for one repo at a time.');
60
+ process.exit(1);
61
+ }
62
+
63
+ const before = machineRepos(manifest, state).map((r) => r.name);
64
+ const { repos } = await chooseRepos({ manifest, root, state, opts: { ...opts, pick: true } });
65
+ const after = repos.map((r) => r.name);
66
+ const { added, dropped } = changes(before, after);
67
+
68
+ heading('This machine keeps');
69
+ for (const name of added) ok(`${c.bold(name)} ${c.dim('added')}`);
70
+ for (const name of dropped) {
71
+ skip(`${c.bold(name)} ${c.dim('off the list — the checkout stays where it is')}`);
72
+ }
73
+ plain(c.dim(` ${after.length} repos`));
74
+
75
+ if (!added.length || opts.clone === false) return;
76
+
77
+ // The selection is on disk now, so clone reads it back and never re-asks.
78
+ await clone.run({ ...opts, pick: false });
79
+ }
package/src/live.js CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  paint,
20
20
  spinner,
21
21
  statusLine,
22
+ truncVisible,
22
23
  visibleWidth,
23
24
  } from './log.js';
24
25
 
@@ -95,10 +96,18 @@ export function board(items, { live = process.stdout.isTTY } = {}) {
95
96
  drawn = 0;
96
97
  };
97
98
 
99
+ // One line, one row. A line wider than the window wraps, and then `drawn` —
100
+ // a count of lines, not of rows — climbs back too few rows and the next frame
101
+ // is drawn below the last one instead of over it. That is how the block ends
102
+ // up repeating itself down the screen with half-cleared remnants in between.
103
+ // The last column is left empty: a line filling the row exactly leaves some
104
+ // terminals in a deferred-wrap state that the following newline then spends.
105
+ const fitRow = (l) => truncVisible(l, Math.max(1, (process.stdout.columns || 80) - 1));
106
+
98
107
  const draw = () => {
99
108
  const body = lines();
100
109
  let s = (drawn ? ansi.up(drawn) : '') + ansi.cr;
101
- for (const l of body) s += ansi.clearLine + l + '\n';
110
+ for (const l of body) s += ansi.clearLine + fitRow(l) + '\n';
102
111
  // The previous block may have been taller — clear what is left of it, then
103
112
  // put the cursor back under the new one.
104
113
  const extra = Math.max(0, drawn - body.length);
package/src/log.js CHANGED
@@ -17,11 +17,12 @@ import {
17
17
  padEndVisible,
18
18
  spinner,
19
19
  stripAnsi,
20
+ truncVisible,
20
21
  useColor,
21
22
  visibleWidth,
22
23
  } from './theme.js';
23
24
 
24
- export { ansi, glyph, padEndVisible, paint, spinner, useColor, visibleWidth };
25
+ export { ansi, glyph, padEndVisible, paint, spinner, truncVisible, useColor, visibleWidth };
25
26
 
26
27
  /**
27
28
  * Named colours, kept because every command's `help` string and most inline
package/src/theme.js CHANGED
@@ -136,3 +136,37 @@ export function centerVisible(s, width) {
136
136
 
137
137
  /** Terminal width, clamped to something a human can read across. */
138
138
  export const columns = () => Math.max(40, Math.min(process.stdout.columns || 80, 120));
139
+
140
+ /**
141
+ * Cut a string to `width` visible columns, leaving its escape codes intact.
142
+ *
143
+ * The live block redraws by counting the lines it wrote and climbing back up
144
+ * that many. A line wider than the window wraps onto two rows, so the count is
145
+ * short by one and every redraw lands a row lower — the block duplicates itself
146
+ * down the screen. Truncating keeps one line to one row.
147
+ */
148
+ export function truncVisible(s, width) {
149
+ const str = String(s);
150
+ if (width <= 0) return '';
151
+ if (visibleWidth(str) <= width) return str;
152
+ let left = width;
153
+ let out = '';
154
+ // eslint-disable-next-line no-control-regex
155
+ for (const part of str.split(/(\x1b\[[0-9;?]*[A-Za-z])/)) {
156
+ if (!part) continue;
157
+ if (part.startsWith('\x1b[')) {
158
+ out += part;
159
+ continue;
160
+ }
161
+ const chars = [...part];
162
+ if (chars.length <= left) {
163
+ out += part;
164
+ left -= chars.length;
165
+ } else {
166
+ out += chars.slice(0, left).join('');
167
+ left = 0;
168
+ }
169
+ }
170
+ // Cut mid-colour, so close it — otherwise the colour bleeds down the screen.
171
+ return out + (useColor ? '\x1b[0m' : '');
172
+ }