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 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
- /** Paths still holding markers. Resolved (or deleted) files leave the list as a side effect. */
31
- export async function unresolvedConflicts(root) {
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 ainda não reorganiza a sua pasta. O que fazer com o que foi varrido
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnispace",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Omni CLI (alpha): transforma uma pasta da sua máquina em um espaço Omni e sincroniza entre máquinas.",
5
5
  "type": "module",
6
6
  "bin": {