@ajaykumarnpm/talea 0.4.0 → 0.6.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 +10 -0
- package/README.md +6 -2
- package/package.json +1 -1
- package/skills/talea/SKILL.md +10 -0
- package/src/cli.js +2 -2
- package/src/commands/add.js +9 -4
- package/src/commands/adopt.js +1 -1
- package/src/commands/clone.js +1 -1
- package/src/commands/exec.js +1 -1
- package/src/commands/init.js +3 -0
- package/src/commands/select.js +21 -4
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +4 -1
- package/src/commands/tree.js +2 -2
- package/src/commands/upgrade.js +1 -0
- package/src/commands/where.js +5 -2
- package/src/config.js +16 -2
- package/src/workspace.js +94 -7
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.6.0] - 2026-09-22
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
- `talea pick <repo>` keeps one repo; `talea update` upgrades the CLI
|
|
11
|
+
|
|
12
|
+
## [0.5.0] - 2026-09-22
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- run commands from outside a workspace
|
|
16
|
+
|
|
7
17
|
## [0.4.0] - 2026-09-22
|
|
8
18
|
|
|
9
19
|
### Added
|
package/README.md
CHANGED
|
@@ -46,6 +46,9 @@ talea manifest push # publish the catalogue so the next machine can rea
|
|
|
46
46
|
repos, every org you belong to, and anything shared with you directly. It then
|
|
47
47
|
asks what this machine should keep and fills the tree.
|
|
48
48
|
|
|
49
|
+
Every command works from anywhere after that. Outside the workspace it uses
|
|
50
|
+
the one `init` made; with more than one on the machine, it asks which.
|
|
51
|
+
|
|
49
52
|
## Every machine after that
|
|
50
53
|
|
|
51
54
|
```sh
|
|
@@ -63,6 +66,7 @@ talea sync # clone the new, fast-forward the rest
|
|
|
63
66
|
talea status # branch, clean/dirty, ahead/behind, in one table
|
|
64
67
|
talea add some-repo # keep one more on this machine, and clone it now
|
|
65
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
|
|
66
70
|
cd $(talea where eklavya)
|
|
67
71
|
```
|
|
68
72
|
|
|
@@ -164,7 +168,7 @@ Treat the id like a bookmark you would not paste into a public channel.
|
|
|
164
168
|
| `talea clone` | clone only — never fetches or merges |
|
|
165
169
|
| `talea adopt` | move checkouts you already have into place |
|
|
166
170
|
| `talea status` | branch, clean/dirty, ahead/behind |
|
|
167
|
-
| `talea select` | reopen the checklist —
|
|
171
|
+
| `talea select` | reopen the checklist — `talea pick <repo>` for one |
|
|
168
172
|
| `talea add` / `talea rm` | change that one repo at a time |
|
|
169
173
|
| `talea where <repo>` | print a repo's path, for `cd $( )` |
|
|
170
174
|
| `talea list` | the catalogue |
|
|
@@ -173,7 +177,7 @@ Treat the id like a bookmark you would not paste into a public channel.
|
|
|
173
177
|
| `talea manifest push/pull` | move the catalogue between machines |
|
|
174
178
|
| `talea skill` | install the skill that lets your coding agent drive talea |
|
|
175
179
|
| `talea doctor` | check this machine can do the work |
|
|
176
|
-
| `talea upgrade` | update the CLI itself |
|
|
180
|
+
| `talea upgrade` | update the CLI itself (also `talea update`) |
|
|
177
181
|
|
|
178
182
|
Every one of them takes `-g <group>` and `-r <repo>` to narrow the run, and
|
|
179
183
|
`--help` for its own examples.
|
package/package.json
CHANGED
package/skills/talea/SKILL.md
CHANGED
|
@@ -46,6 +46,10 @@ It exits **non-zero** on an unknown name and writes every diagnostic to stderr,
|
|
|
46
46
|
so `cd "$(talea where typo)"` fails instead of landing in the home directory.
|
|
47
47
|
Never `cd` to a path it did not print.
|
|
48
48
|
|
|
49
|
+
Outside a workspace it falls back to the ones this machine has. With several
|
|
50
|
+
and no terminal — which is how you run it — it lists them and exits non-zero.
|
|
51
|
+
`cd` into the workspace the user means rather than guessing from the list.
|
|
52
|
+
|
|
49
53
|
Two owners can own a repo of the same name. When that happens it says so and
|
|
50
54
|
exits non-zero — pass `owner/name` rather than picking one.
|
|
51
55
|
|
|
@@ -113,11 +117,17 @@ and nothing in talea will do it for them.
|
|
|
113
117
|
talea add some-repo # keep one more here, and clone it now
|
|
114
118
|
talea rm some-repo # stop keeping it — the checkout stays exactly where it is
|
|
115
119
|
talea select # reopen the whole checklist, interactively
|
|
120
|
+
talea pick some-repo # = add when the name is exact; otherwise the checklist
|
|
116
121
|
```
|
|
117
122
|
|
|
118
123
|
`talea select` needs a terminal. In a non-interactive session it prints a
|
|
119
124
|
summary instead of hanging, so prefer `add` and `rm` when acting on the
|
|
120
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.
|
|
121
131
|
|
|
122
132
|
`rm` does **not** delete the checkout. Say that plainly when you run it, or it
|
|
123
133
|
reads like data loss.
|
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,6 +62,7 @@ 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
|
|
|
@@ -110,7 +110,7 @@ ${c.bold('Commands')}
|
|
|
110
110
|
${c.cyan('clone')} clone only — never fetches or merges
|
|
111
111
|
${c.cyan('adopt')} move repos you already have into the right place
|
|
112
112
|
${c.cyan('status')} one table: branch, clean/dirty, ahead/behind
|
|
113
|
-
${c.cyan('select')} reopen the checklist —
|
|
113
|
+
${c.cyan('select')} reopen the checklist — or ${c.dim('talea pick <repo>')} for one
|
|
114
114
|
${c.cyan('add')} keep another repo on this machine (${c.dim('rm')} to drop one)
|
|
115
115
|
${c.cyan('where')} print a repo's path — ${c.dim('cd $(talea where eklavya)')}
|
|
116
116
|
${c.cyan('list')} show the catalogue
|
package/src/commands/add.js
CHANGED
|
@@ -34,12 +34,17 @@ Options
|
|
|
34
34
|
-j, --jobs <n> parallel clones
|
|
35
35
|
`;
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
export function
|
|
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
|
-
|
|
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.`);
|
|
@@ -54,7 +59,7 @@ export function resolve(manifest, name) {
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
export async function run(opts, positionals = []) {
|
|
57
|
-
const { root, manifest, state } = requireWorkspace();
|
|
62
|
+
const { root, manifest, state } = await requireWorkspace();
|
|
58
63
|
requireCatalogue(manifest);
|
|
59
64
|
|
|
60
65
|
if (!positionals.length) {
|
package/src/commands/adopt.js
CHANGED
|
@@ -322,7 +322,7 @@ export function parseFromPaths(from) {
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
export async function run(opts) {
|
|
325
|
-
const { root, manifest, state } = requireWorkspace();
|
|
325
|
+
const { root, manifest, state } = await requireWorkspace();
|
|
326
326
|
|
|
327
327
|
if (opts['fix-paths']) {
|
|
328
328
|
heading('Repairing paths for repos already adopted');
|
package/src/commands/clone.js
CHANGED
|
@@ -200,7 +200,7 @@ export function writeDocs(manifest, root, repos) {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
export async function run(opts) {
|
|
203
|
-
const { root, manifest, state } = requireWorkspace();
|
|
203
|
+
const { root, manifest, state } = await requireWorkspace();
|
|
204
204
|
requireCatalogue(manifest);
|
|
205
205
|
|
|
206
206
|
const protocol = opts.protocol ?? state.protocol ?? 'ssh';
|
package/src/commands/exec.js
CHANGED
|
@@ -46,7 +46,7 @@ export async function run(opts, positionals) {
|
|
|
46
46
|
process.exit(1);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const { root, manifest, state } = requireWorkspace();
|
|
49
|
+
const { root, manifest, state } = await requireWorkspace();
|
|
50
50
|
const pool = opts.all ? manifest.repos : machineRepos(manifest, state);
|
|
51
51
|
const entries = clonedOnly(withPaths(manifest, root, selectRepos(manifest, opts, pool)));
|
|
52
52
|
|
package/src/commands/init.js
CHANGED
|
@@ -15,6 +15,7 @@ import { samePath } from '../adopt.js';
|
|
|
15
15
|
import { c, context, fail, heading, info, ok, plain, skip, warn } from '../log.js';
|
|
16
16
|
import { run as discover } from './discover.js';
|
|
17
17
|
import { run as sync } from './sync.js';
|
|
18
|
+
import { rememberWorkspace } from '../workspace.js';
|
|
18
19
|
|
|
19
20
|
export const help = `
|
|
20
21
|
${c.bold('talea init')} — set this machine up
|
|
@@ -105,6 +106,8 @@ export async function run(opts, positionals = []) {
|
|
|
105
106
|
} else {
|
|
106
107
|
skip(`${STATE_FILE} already here, leaving it alone`);
|
|
107
108
|
}
|
|
109
|
+
// So `talea sync` run from outside any workspace knows this one exists.
|
|
110
|
+
rememberWorkspace(target);
|
|
108
111
|
|
|
109
112
|
let manifest = loadManifest(target);
|
|
110
113
|
|
package/src/commands/select.js
CHANGED
|
@@ -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
|
-
|
|
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,8 +58,8 @@ export function changes(before, after) {
|
|
|
47
58
|
};
|
|
48
59
|
}
|
|
49
60
|
|
|
50
|
-
export async function run(opts) {
|
|
51
|
-
const { root, manifest, state } = requireWorkspace();
|
|
61
|
+
export async function run(opts, positionals = []) {
|
|
62
|
+
const { root, manifest, state } = await requireWorkspace();
|
|
52
63
|
requireCatalogue(manifest);
|
|
53
64
|
|
|
54
65
|
// -g/-r narrow a run; they cannot narrow a decision about the whole machine.
|
|
@@ -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);
|
package/src/commands/status.js
CHANGED
|
@@ -29,7 +29,7 @@ Options
|
|
|
29
29
|
`;
|
|
30
30
|
|
|
31
31
|
export async function run(opts) {
|
|
32
|
-
const { root, manifest, state } = requireWorkspace();
|
|
32
|
+
const { root, manifest, state } = await requireWorkspace();
|
|
33
33
|
const pool = opts.all ? manifest.repos : machineRepos(manifest, state);
|
|
34
34
|
const entries = withPaths(manifest, root, selectRepos(manifest, opts, pool));
|
|
35
35
|
|
package/src/commands/sync.js
CHANGED
|
@@ -31,6 +31,9 @@ your default set, or a checklist of everything in the catalogue with those
|
|
|
31
31
|
defaults already ticked. The answer is remembered in ${c.dim('.talea.json')}, so every run
|
|
32
32
|
after that is a bare ${c.dim('talea sync')}.
|
|
33
33
|
|
|
34
|
+
It runs from anywhere. Inside a workspace it uses that one; outside, it uses
|
|
35
|
+
the workspace ${c.dim('talea init')} made, or asks which when this machine has several.
|
|
36
|
+
|
|
34
37
|
Options
|
|
35
38
|
-g, --group <names> comma-separated groups
|
|
36
39
|
-r, --repo <names> comma-separated repo names
|
|
@@ -53,7 +56,7 @@ leaves it alone, and says which.
|
|
|
53
56
|
`;
|
|
54
57
|
|
|
55
58
|
export async function run(opts) {
|
|
56
|
-
const { root, manifest, state } = requireWorkspace();
|
|
59
|
+
const { root, manifest, state } = await requireWorkspace();
|
|
57
60
|
requireCatalogue(manifest);
|
|
58
61
|
|
|
59
62
|
const protocol = opts.protocol ?? state.protocol ?? 'ssh';
|
package/src/commands/tree.js
CHANGED
|
@@ -30,8 +30,8 @@ Options
|
|
|
30
30
|
|
|
31
31
|
const hasDoc = (dir) => existsSync(path.join(dir, 'CLAUDE.md'));
|
|
32
32
|
|
|
33
|
-
export function run(opts) {
|
|
34
|
-
const { root, manifest, state } = requireWorkspace();
|
|
33
|
+
export async function run(opts) {
|
|
34
|
+
const { root, manifest, state } = await requireWorkspace();
|
|
35
35
|
const pool = opts.all ? manifest.repos : machineRepos(manifest, state);
|
|
36
36
|
const entries = withPaths(manifest, root, selectRepos(manifest, opts, pool));
|
|
37
37
|
|
package/src/commands/upgrade.js
CHANGED
|
@@ -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/commands/where.js
CHANGED
|
@@ -17,10 +17,13 @@ composes with ${c.dim('cd')}, ${c.dim('code')}, ${c.dim('open')} and anything el
|
|
|
17
17
|
|
|
18
18
|
Exits non-zero if the repo is not in the catalogue, so ${c.dim('cd $(talea where typo)')}
|
|
19
19
|
fails loudly instead of landing you in your home directory.
|
|
20
|
+
|
|
21
|
+
Works from outside a workspace too: with one on this machine it uses that, with
|
|
22
|
+
several it asks — on stderr, so the answer never ends up in the path.
|
|
20
23
|
`;
|
|
21
24
|
|
|
22
|
-
export function run(opts, positionals = []) {
|
|
23
|
-
const { root, manifest } = requireWorkspace();
|
|
25
|
+
export async function run(opts, positionals = []) {
|
|
26
|
+
const { root, manifest } = await requireWorkspace();
|
|
24
27
|
const name = positionals[0];
|
|
25
28
|
|
|
26
29
|
if (!name) {
|
package/src/config.js
CHANGED
|
@@ -33,7 +33,7 @@ export const USER_STATE = path.join(USER_DIR, 'state.json');
|
|
|
33
33
|
|
|
34
34
|
const readJson = (file) => JSON.parse(readFileSync(file, 'utf8'));
|
|
35
35
|
|
|
36
|
-
/** Machine-wide state: the update-check stamp, the gist id. Not per workspace. */
|
|
36
|
+
/** Machine-wide state: the update-check stamp, the gist id, the workspace list. Not per workspace. */
|
|
37
37
|
export function readUserState() {
|
|
38
38
|
try {
|
|
39
39
|
return readJson(USER_STATE);
|
|
@@ -48,7 +48,8 @@ export function writeUserState(state) {
|
|
|
48
48
|
writeFileSync(USER_STATE, JSON.stringify(state, null, 2) + '\n');
|
|
49
49
|
} catch {
|
|
50
50
|
// A read-only home directory must not break the actual command. The only
|
|
51
|
-
// things kept here are a cache stamp
|
|
51
|
+
// things kept here are a cache stamp, a gist id and the workspace list,
|
|
52
|
+
// and a lost list re-fills itself the next time a command runs inside one.
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -95,6 +96,19 @@ export function findWorkspace(start = process.cwd()) {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Every workspace this machine has set up, as recorded in ~/.talea/state.json.
|
|
101
|
+
*
|
|
102
|
+
* This is what lets `talea sync` run from anywhere: outside a workspace the
|
|
103
|
+
* upward walk finds nothing, and this list is what is left to ask. Kept in the
|
|
104
|
+
* machine-wide state rather than the catalogue because a path on this laptop
|
|
105
|
+
* means nothing on the next one. Entries whose .talea.json has gone are skipped
|
|
106
|
+
* on read, not dropped on write — a deleted workspace stops being offered, but
|
|
107
|
+
* an unplugged drive that comes back has not been forgotten.
|
|
108
|
+
*/
|
|
109
|
+
export const knownWorkspaces = () =>
|
|
110
|
+
(readUserState().workspaces ?? []).filter((dir) => existsSync(path.join(dir, STATE_FILE)));
|
|
111
|
+
|
|
98
112
|
export function loadState(workspaceRoot) {
|
|
99
113
|
const file = path.join(workspaceRoot, STATE_FILE);
|
|
100
114
|
return existsSync(file) ? readJson(file) : {};
|
package/src/workspace.js
CHANGED
|
@@ -4,9 +4,25 @@
|
|
|
4
4
|
// Every command that acts on repos goes through here, so `clone`, `sync`,
|
|
5
5
|
// `status` and `exec` filter identically.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import readline from 'node:readline/promises';
|
|
11
|
+
|
|
12
|
+
import { samePath } from './adopt.js';
|
|
13
|
+
import {
|
|
14
|
+
STATE_FILE,
|
|
15
|
+
findWorkspace,
|
|
16
|
+
knownWorkspaces,
|
|
17
|
+
loadManifest,
|
|
18
|
+
loadState,
|
|
19
|
+
readUserState,
|
|
20
|
+
repoDir,
|
|
21
|
+
repoGroup,
|
|
22
|
+
writeUserState,
|
|
23
|
+
} from './config.js';
|
|
8
24
|
import { isRepo } from './git.js';
|
|
9
|
-
import { fail } from './log.js';
|
|
25
|
+
import { c, fail } from './log.js';
|
|
10
26
|
|
|
11
27
|
const csv = (v) =>
|
|
12
28
|
(Array.isArray(v) ? v : [v])
|
|
@@ -16,16 +32,87 @@ const csv = (v) =>
|
|
|
16
32
|
.filter(Boolean);
|
|
17
33
|
|
|
18
34
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
35
|
+
* Add a workspace to this machine's list, once.
|
|
36
|
+
*
|
|
37
|
+
* Compared with samePath, not as strings: on macOS and Windows `~/workspace`
|
|
38
|
+
* and `~/Workspace` are one folder, and cd-ing in with the other spelling would
|
|
39
|
+
* otherwise add it again on every run.
|
|
40
|
+
*/
|
|
41
|
+
export function rememberWorkspace(root) {
|
|
42
|
+
const state = readUserState();
|
|
43
|
+
const list = state.workspaces ?? [];
|
|
44
|
+
if (list.some((dir) => samePath(dir, root))) return;
|
|
45
|
+
writeUserState({ ...state, workspaces: [...list, root] });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The workspaces a command run from outside any of them could mean: every one
|
|
50
|
+
* `init` has recorded, plus the default `~/Workspace` — which is how a
|
|
51
|
+
* workspace made before the list existed is still found from anywhere.
|
|
21
52
|
*/
|
|
22
|
-
export function
|
|
23
|
-
const
|
|
24
|
-
|
|
53
|
+
export function workspaceCandidates(known = knownWorkspaces(), manifest = loadManifest(null)) {
|
|
54
|
+
const fallback = path.join(os.homedir(), manifest.workspace || 'Workspace');
|
|
55
|
+
const all = existsSync(path.join(fallback, STATE_FILE)) ? [...known, fallback] : known;
|
|
56
|
+
return all.filter((dir, i) => all.findIndex((d) => samePath(d, dir)) === i);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Outside a workspace: one known workspace is used, several are asked about.
|
|
61
|
+
*
|
|
62
|
+
* Everything here goes to stderr — `cd $(talea where)` reads stdout, and a
|
|
63
|
+
* prompt or a note there is a folder the shell tries to enter. With no
|
|
64
|
+
* terminal to ask on, it stops rather than picks: a script that syncs whichever
|
|
65
|
+
* workspace happened to be first is running against a tree nobody named.
|
|
66
|
+
*/
|
|
67
|
+
async function pickKnownWorkspace() {
|
|
68
|
+
const found = workspaceCandidates();
|
|
69
|
+
|
|
70
|
+
if (!found.length) {
|
|
25
71
|
fail('Not inside a talea workspace (no .talea.json found).');
|
|
26
72
|
console.error('\n Run `talea init` to create one, or cd into an existing workspace.');
|
|
27
73
|
process.exit(1);
|
|
28
74
|
}
|
|
75
|
+
if (found.length === 1) {
|
|
76
|
+
console.error(c.dim(`Using the workspace at ${found[0]}\n`));
|
|
77
|
+
return found[0];
|
|
78
|
+
}
|
|
79
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
80
|
+
fail(`Not inside a talea workspace, and this machine has ${found.length}.`);
|
|
81
|
+
for (const dir of found) console.error(` ${c.bold(dir)}`);
|
|
82
|
+
console.error('\n cd into the one you mean.');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.error(`\n${c.bold('Which workspace?')}`);
|
|
87
|
+
found.forEach((dir, i) => console.error(` ${c.cyan(String(i + 1))} ${dir}`));
|
|
88
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
89
|
+
const answer = (await rl.question(`\n${c.dim(`[1-${found.length}]`)} `)).trim();
|
|
90
|
+
rl.close();
|
|
91
|
+
|
|
92
|
+
// A wrong answer stops, same as a typo'd repo name: guessing here would run
|
|
93
|
+
// a bulk command against a tree the developer did not choose.
|
|
94
|
+
const chosen = found[Number(answer) - 1];
|
|
95
|
+
if (!/^\d+$/.test(answer) || !chosen) {
|
|
96
|
+
fail(`"${answer}" is not one of 1-${found.length}.`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
console.error('');
|
|
100
|
+
return chosen;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the workspace, manifest and state, or exit with a useful message.
|
|
105
|
+
* Commands that need an initialised workspace call this first.
|
|
106
|
+
*
|
|
107
|
+
* Inside a workspace the upward walk wins, exactly as git's does. Outside one,
|
|
108
|
+
* the workspaces this machine knows about are offered instead.
|
|
109
|
+
*/
|
|
110
|
+
export async function requireWorkspace() {
|
|
111
|
+
const inside = findWorkspace();
|
|
112
|
+
// Recorded on every run from inside, so a workspace made before the list
|
|
113
|
+
// existed joins it the first time anything is run there.
|
|
114
|
+
if (inside) rememberWorkspace(inside);
|
|
115
|
+
const root = inside ?? (await pickKnownWorkspace());
|
|
29
116
|
const manifest = loadManifest(root);
|
|
30
117
|
const state = loadState(root);
|
|
31
118
|
return { root, manifest, state };
|