omnispace 0.6.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/README.md +7 -1
- package/dist/conflicts.js +6 -3
- package/dist/index.js +6 -2
- package/dist/status.js +159 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
CLI do [Omni](https://omni.tzolkin.cloud): transforma uma pasta da sua máquina em um espaço Omni e sincroniza esse espaço entre máquinas.
|
|
4
4
|
|
|
5
|
-
> Alpha. Existem `login`, `logout`, `whoami`, `init`, `sync --push`, `sync --pull` e `clone`.
|
|
5
|
+
> Alpha. Existem `login`, `logout`, `whoami`, `init`, `status`, `sync --push`, `sync --pull` e `clone`.
|
|
6
6
|
|
|
7
7
|
Precisa do Node 20 ou mais novo.
|
|
8
8
|
|
|
@@ -34,6 +34,12 @@ Só ler a pasta, sem vincular nem enviar nada:
|
|
|
34
34
|
npx -y omnispace@latest init . --scan-only
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
Ver o que mudou nesta pasta e no servidor, e qual o próximo comando (só lê, não altera nada):
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
npx -y omnispace@latest status
|
|
41
|
+
```
|
|
42
|
+
|
|
37
43
|
Publicar uma nova versão do espaço no servidor:
|
|
38
44
|
|
|
39
45
|
```
|
package/dist/conflicts.js
CHANGED
|
@@ -27,8 +27,11 @@ export async function addConflicts(root, paths) {
|
|
|
27
27
|
if (paths.length > 0)
|
|
28
28
|
await writeList(root, [...(await readList(root)), ...paths]);
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Paths still holding markers. Resolved (or deleted) files leave the list as a side effect,
|
|
32
|
+
* unless `readOnly` (omnispace status never writes to the folder).
|
|
33
|
+
*/
|
|
34
|
+
export async function unresolvedConflicts(root, opts = {}) {
|
|
32
35
|
const list = await readList(root);
|
|
33
36
|
const pending = [];
|
|
34
37
|
for (const path of list) {
|
|
@@ -40,7 +43,7 @@ export async function unresolvedConflicts(root) {
|
|
|
40
43
|
// file gone: nothing left to resolve
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
|
-
if (pending.length !== list.length)
|
|
46
|
+
if (!opts.readOnly && pending.length !== list.length)
|
|
44
47
|
await writeList(root, pending);
|
|
45
48
|
return pending;
|
|
46
49
|
}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { LockedError } from "./lock.js";
|
|
|
13
13
|
import { defaultApiUrl, insecureApiUrl } from "./config.js";
|
|
14
14
|
import { login, logout, printWhoami, whoami } from "./login.js";
|
|
15
15
|
import { initWithAccount } from "./init-account.js";
|
|
16
|
+
import { statusSpace } from "./status.js";
|
|
16
17
|
const USAGE = `
|
|
17
18
|
Omni CLI
|
|
18
19
|
|
|
@@ -23,13 +24,14 @@ Uso:
|
|
|
23
24
|
omni init [pasta] [--nome <nome>] com a CLI logada, cria o espaço na sua conta e liga a pasta
|
|
24
25
|
omni init [pasta] --scan-only varre a pasta e mostra o que encontrou
|
|
25
26
|
omni init [pasta] --link <código> varre a pasta e envia pro código mostrado no navegador
|
|
27
|
+
omni status [pasta] o que mudou aqui e no servidor, e o próximo comando (só lê)
|
|
26
28
|
omni sync [pasta] --push [--yes] publica uma nova versão do espaço no servidor
|
|
27
29
|
omni sync [pasta] --pull traz para a pasta a versão mais nova do servidor
|
|
28
30
|
[--conflito=local|mesclar|backup] mesma resposta para todos os conflitos, sem perguntar
|
|
29
31
|
omni clone <código> <pasta> baixa um espaço da sua conta numa pasta nova desta máquina
|
|
30
32
|
|
|
31
|
-
A CLI
|
|
32
|
-
é decidido na tela de revisão, no navegador.
|
|
33
|
+
A CLI não reorganiza a sua pasta. Com --link, o que fazer com o que foi
|
|
34
|
+
varrido é decidido na tela de revisão, no navegador.
|
|
33
35
|
`;
|
|
34
36
|
async function main(argv) {
|
|
35
37
|
const args = argv.slice(2);
|
|
@@ -53,6 +55,8 @@ async function main(argv) {
|
|
|
53
55
|
return runSync(args.slice(1));
|
|
54
56
|
if (command === "clone")
|
|
55
57
|
return runClone(args.slice(1));
|
|
58
|
+
if (command === "status")
|
|
59
|
+
return statusSpace(resolve(args.slice(1).find((a) => !a.startsWith("-")) ?? process.cwd()));
|
|
56
60
|
if (command !== "init") {
|
|
57
61
|
console.error(`Comando desconhecido: ${command}`);
|
|
58
62
|
console.log(USAGE.trim());
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { callApi } from "./api.js";
|
|
3
|
+
import { RETRY_DELAYS_MS } from "./retry.js";
|
|
4
|
+
import { buildManifest } from "./include.js";
|
|
5
|
+
import { unresolvedConflicts } from "./conflicts.js";
|
|
6
|
+
import { readJournal } from "./journal.js";
|
|
7
|
+
import { planPull } from "./pull.js";
|
|
8
|
+
import { loadToken, readSpaceConfig } from "./config.js";
|
|
9
|
+
/**
|
|
10
|
+
* What `omnispace status` reports, from the same three-way comparison the pull uses (ADR 002).
|
|
11
|
+
* A path equal on both sides (same edit made twice) counts as nothing to do.
|
|
12
|
+
*/
|
|
13
|
+
export function summarizeStatus(local, base, head) {
|
|
14
|
+
const plan = planPull(local, base, head);
|
|
15
|
+
const kind = (path) => !base.has(path) ? "nova" : !local.has(path) ? "apagada" : "editada";
|
|
16
|
+
return {
|
|
17
|
+
local: [...plan.localOnly, ...plan.conflicts].sort().map((path) => ({ path, kind: kind(path) })),
|
|
18
|
+
remote: plan.writes.length + plan.deletes.length + plan.conflicts.length,
|
|
19
|
+
both: plan.conflicts,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** The commands to run next, in order. Empty when the folder and the server agree. */
|
|
23
|
+
export function nextSteps(opts) {
|
|
24
|
+
const { head, baseVersion, summary, markers, pullInterrupted } = opts;
|
|
25
|
+
if (pullInterrupted)
|
|
26
|
+
return ["omnispace sync --pull (termina o pull que parou no meio)"];
|
|
27
|
+
if (markers.length > 0) {
|
|
28
|
+
return ["resolva os marcadores de conflito nos arquivos acima", "omnispace sync --push"];
|
|
29
|
+
}
|
|
30
|
+
const steps = [];
|
|
31
|
+
if (head > baseVersion) {
|
|
32
|
+
steps.push(summary.both.length > 0
|
|
33
|
+
? `omnispace sync --pull (vai pedir decisão em ${summary.both.length} arquivo(s) mudado(s) dos dois lados)`
|
|
34
|
+
: "omnispace sync --pull");
|
|
35
|
+
}
|
|
36
|
+
if (summary.local.length > 0)
|
|
37
|
+
steps.push("omnispace sync --push");
|
|
38
|
+
return steps;
|
|
39
|
+
}
|
|
40
|
+
export function timeAgo(iso, now = Date.now()) {
|
|
41
|
+
const minutes = Math.round((now - new Date(iso).getTime()) / 60000);
|
|
42
|
+
if (minutes < 1)
|
|
43
|
+
return "agora";
|
|
44
|
+
if (minutes < 60)
|
|
45
|
+
return `há ${minutes} min`;
|
|
46
|
+
const hours = Math.round(minutes / 60);
|
|
47
|
+
if (hours < 24)
|
|
48
|
+
return `há ${hours} h`;
|
|
49
|
+
return `há ${Math.round(hours / 24)} d`;
|
|
50
|
+
}
|
|
51
|
+
const KIND_MARK = { nova: "+", editada: "~", apagada: "-" };
|
|
52
|
+
function listChanges(changes, max = 20) {
|
|
53
|
+
for (const change of changes.slice(0, max))
|
|
54
|
+
console.log(` ${KIND_MARK[change.kind]} ${change.path}`);
|
|
55
|
+
if (changes.length > max)
|
|
56
|
+
console.log(` ... e mais ${changes.length - max}`);
|
|
57
|
+
}
|
|
58
|
+
function countKinds(changes) {
|
|
59
|
+
const parts = ["editada", "nova", "apagada"]
|
|
60
|
+
.map((k) => [k, changes.filter((c) => c.kind === k).length])
|
|
61
|
+
.filter(([, n]) => n > 0)
|
|
62
|
+
.map(([k, n]) => `${n} ${n === 1 ? k : `${k}s`}`);
|
|
63
|
+
return parts.join(", ");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* `omnispace status` (P1, docs/plan/p1-continuidade-visivel.md): what this folder has that the
|
|
67
|
+
* server does not, what the server has that the folder does not, and the next command. Read only:
|
|
68
|
+
* no lock, nothing written to the folder or to `.omni/`.
|
|
69
|
+
*/
|
|
70
|
+
export async function statusSpace(root) {
|
|
71
|
+
const config = await readSpaceConfig(root);
|
|
72
|
+
if (!config) {
|
|
73
|
+
console.log(`\n${basename(root)} não é um espaço Omni (não tem .omni/space.json).`);
|
|
74
|
+
console.log("Para transformar esta pasta num espaço: omnispace init\n");
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
if (!config.spaceId) {
|
|
78
|
+
console.log("\nEsta pasta foi vinculada pelo navegador, mas o espaço ainda não foi criado.");
|
|
79
|
+
console.log("Termine a conta no navegador e depois rode: omnispace sync --push\n");
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
const token = await loadToken(config.apiUrl);
|
|
83
|
+
if (!token) {
|
|
84
|
+
console.log(`\nNenhuma sessão da CLI nesta máquina para ${config.apiUrl}.`);
|
|
85
|
+
console.log("Para entrar: omnispace login\n");
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
const [journal, markers] = await Promise.all([readJournal(root), unresolvedConflicts(root, { readOnly: true })]);
|
|
89
|
+
console.log("\n→ Lendo a pasta...");
|
|
90
|
+
const manifest = await buildManifest(root);
|
|
91
|
+
if (manifest.truncated) {
|
|
92
|
+
console.error("Pasta grande ou funda demais para comparar (limite de 20.000 arquivos e 12 níveis).");
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
const res = await callApi(config.apiUrl, token, "/api/cli/sync/status", { spaceId: config.spaceId, baseVersion: config.baseVersion }, { retryDelaysMs: RETRY_DELAYS_MS });
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
console.log(`\nServidor não consultado: ${res.error}`);
|
|
98
|
+
if (res.expired || res.status === 401)
|
|
99
|
+
console.log("Para entrar de novo: omnispace login");
|
|
100
|
+
console.log(`Esta pasta está na versão ${config.baseVersion}. Sem o servidor não dá para dizer o que mudou.\n`);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
const head = res.head;
|
|
104
|
+
const headMap = new Map(res.files.map((f) => [f.path, f.hash]));
|
|
105
|
+
const baseMap = res.base ? new Map(res.base.map((f) => [f.path, f.hash])) : headMap;
|
|
106
|
+
const local = new Map(manifest.files.map((f) => [f.path, f.hash]));
|
|
107
|
+
// An unreadable file is missing from the manifest; left in, it would show up as deleted here.
|
|
108
|
+
const skip = (path) => manifest.unreadable.some((u) => path === u || path.startsWith(`${u}/`));
|
|
109
|
+
const readable = (map) => new Map([...map].filter(([path]) => !skip(path)));
|
|
110
|
+
const summary = summarizeStatus(local, readable(baseMap), readable(headMap));
|
|
111
|
+
console.log(`\nEspaço: ${res.name} (${res.slug})`);
|
|
112
|
+
console.log(`Nesta pasta: ${config.baseVersion === 0 ? "nenhuma versão ainda" : `versão ${config.baseVersion}`}`);
|
|
113
|
+
if (head === 0) {
|
|
114
|
+
console.log("No servidor: nenhuma versão publicada ainda");
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const when = res.publishedAt ? `, publicada ${timeAgo(res.publishedAt)}` : "";
|
|
118
|
+
const who = res.publishedBy ? ` por ${res.publishedBy}` : "";
|
|
119
|
+
console.log(`No servidor: versão ${head}${when}${who}`);
|
|
120
|
+
}
|
|
121
|
+
if (summary.local.length > 0) {
|
|
122
|
+
console.log(`\nMudanças nesta pasta ainda não enviadas: ${summary.local.length} (${countKinds(summary.local)})`);
|
|
123
|
+
listChanges(summary.local);
|
|
124
|
+
}
|
|
125
|
+
if (head > config.baseVersion) {
|
|
126
|
+
console.log(`\nNo servidor há ${summary.remote} arquivo(s) diferente(s) da versão desta pasta.`);
|
|
127
|
+
}
|
|
128
|
+
if (summary.both.length > 0) {
|
|
129
|
+
console.log(`\nMudados dos dois lados (o pull pergunta o que fazer):`);
|
|
130
|
+
for (const path of summary.both.slice(0, 20))
|
|
131
|
+
console.log(` ${path}`);
|
|
132
|
+
}
|
|
133
|
+
if (markers.length > 0) {
|
|
134
|
+
console.log("\nAinda com marcadores de conflito (<<<<<<< esta máquina), o push recusa até serem editados:");
|
|
135
|
+
for (const path of markers)
|
|
136
|
+
console.log(` ${path}`);
|
|
137
|
+
}
|
|
138
|
+
if (manifest.unreadable.length > 0) {
|
|
139
|
+
console.log(`\n${manifest.unreadable.length} arquivo(s) não puderam ser lidos (abertos em outro programa?); a comparação ignora esses.`);
|
|
140
|
+
}
|
|
141
|
+
const steps = nextSteps({
|
|
142
|
+
head,
|
|
143
|
+
baseVersion: config.baseVersion,
|
|
144
|
+
summary,
|
|
145
|
+
markers,
|
|
146
|
+
pullInterrupted: journal !== null,
|
|
147
|
+
});
|
|
148
|
+
if (journal)
|
|
149
|
+
console.log(`\nUm pull da versão ${journal.head} parou no meio.`);
|
|
150
|
+
if (steps.length === 0) {
|
|
151
|
+
console.log(head === 0 ? "\n✓ Pasta vazia e nada publicado ainda.\n" : `\n✓ Em dia com a versão ${head}.\n`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
console.log("\nPróximo passo:");
|
|
155
|
+
steps.forEach((step, i) => console.log(` ${i + 1}. ${step}`));
|
|
156
|
+
console.log("");
|
|
157
|
+
}
|
|
158
|
+
return 0;
|
|
159
|
+
}
|