@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 +57 -0
- package/README.md +3 -1
- package/package.json +3 -2
- package/src/cli.js +5 -0
- package/src/commands/select.js +79 -0
- package/src/live.js +10 -1
- package/src/log.js +2 -1
- package/src/theme.js +34 -0
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
|
|
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.
|
|
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
|
+
}
|