@ajaykumarnpm/talea 0.4.0 → 0.5.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 +5 -0
- package/README.md +3 -0
- package/package.json +1 -1
- package/skills/talea/SKILL.md +4 -0
- package/src/commands/add.js +1 -1
- 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 +1 -1
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +4 -1
- package/src/commands/tree.js +2 -2
- 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,11 @@ 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.5.0] - 2026-09-22
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- run commands from outside a workspace
|
|
11
|
+
|
|
7
12
|
## [0.4.0] - 2026-09-22
|
|
8
13
|
|
|
9
14
|
### 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
|
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
|
|
package/src/commands/add.js
CHANGED
|
@@ -54,7 +54,7 @@ export function resolve(manifest, name) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export async function run(opts, positionals = []) {
|
|
57
|
-
const { root, manifest, state } = requireWorkspace();
|
|
57
|
+
const { root, manifest, state } = await requireWorkspace();
|
|
58
58
|
requireCatalogue(manifest);
|
|
59
59
|
|
|
60
60
|
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
|
@@ -48,7 +48,7 @@ export function changes(before, after) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export async function run(opts) {
|
|
51
|
-
const { root, manifest, state } = requireWorkspace();
|
|
51
|
+
const { root, manifest, state } = await requireWorkspace();
|
|
52
52
|
requireCatalogue(manifest);
|
|
53
53
|
|
|
54
54
|
// -g/-r narrow a run; they cannot narrow a decision about the whole machine.
|
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/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 };
|