ganbatte-os 0.2.32 → 0.2.34

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.
@@ -30,9 +30,74 @@ function readPackageVersion() {
30
30
 
31
31
  const VERSION = readPackageVersion();
32
32
  const UPSTREAM_REMOTE = 'upstream';
33
- const UPSTREAM_BRANCH = 'main';
33
+ const DEFAULT_UPSTREAM_BRANCH = 'main';
34
34
  const LOCAL_DIR = '.gos-local';
35
35
  const MANIFEST_PATH = '.gos/manifests/gos-install-manifest.json';
36
+ const CONFIG_PATH = '.gos/config.json';
37
+ const CORRECT_UPSTREAM_URL = 'git@github-adriano:adrianomorais-ganbatte/g-os.git';
38
+ const CORRECT_UPSTREAM_URL_HTTPS = 'https://github.com/adrianomorais-ganbatte/g-os.git';
39
+ const STASH_LABEL = 'gos-update-auto-stash';
40
+
41
+ /**
42
+ * Resolve a development branch a partir de .gos/config.json.
43
+ * Fallback: 'main'. Permite override via env GOS_UPSTREAM_BRANCH.
44
+ */
45
+ function resolveUpstreamBranch(root) {
46
+ if (process.env.GOS_UPSTREAM_BRANCH) return process.env.GOS_UPSTREAM_BRANCH;
47
+ const cfg = readJson(path.join(root, CONFIG_PATH));
48
+ return cfg?.defaultBranches?.development || cfg?.defaultBranches?.production || DEFAULT_UPSTREAM_BRANCH;
49
+ }
50
+
51
+ /**
52
+ * Detecta se este workspace é o framework G-OS em si (fork/clone) ou um projeto
53
+ * consumidor que apenas instalou o framework via `gos install`.
54
+ * - 'framework': package.json#name === 'ganbatte-os' AND .git no root
55
+ * - 'consumer': qualquer outro caso
56
+ */
57
+ function detectMode(root) {
58
+ const pkg = readJson(path.join(root, 'package.json'), {});
59
+ const isGosPackage = pkg.name === 'ganbatte-os';
60
+ const hasGit = pathExists(path.join(root, '.git'));
61
+ return isGosPackage && hasGit ? 'framework' : 'consumer';
62
+ }
63
+
64
+ /**
65
+ * Valida que o remote upstream existe E responde. Não modifica nada.
66
+ * Retorna { ok, remoteUrl, error }.
67
+ */
68
+ function validateUpstream(root) {
69
+ const remotes = gitCapture(['remote'], { cwd: root });
70
+ if (!remotes.split(/\r?\n/).includes(UPSTREAM_REMOTE)) {
71
+ return { ok: false, error: `remote "${UPSTREAM_REMOTE}" não configurado` };
72
+ }
73
+ const url = gitCapture(['remote', 'get-url', UPSTREAM_REMOTE], { cwd: root });
74
+ // Detecta URL antiga incorreta (ganbatte-os.git em vez de g-os.git)
75
+ if (/ganbatte-os\.git/.test(url) && !/g-os\.git/.test(url)) {
76
+ return { ok: false, remoteUrl: url, error: 'URL do upstream parece estar com nome antigo (ganbatte-os.git)' };
77
+ }
78
+ // Ping no remoto sem stashar nada
79
+ try {
80
+ execFileSync('git', ['ls-remote', '--quiet', '--exit-code', UPSTREAM_REMOTE, 'HEAD'], {
81
+ cwd: root, stdio: 'pipe', encoding: 'utf8',
82
+ });
83
+ return { ok: true, remoteUrl: url };
84
+ } catch (e) {
85
+ return { ok: false, remoteUrl: url, error: `não foi possível alcançar ${url}` };
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Lista todos stashes auto-criados pelo gos-update.
91
+ * Retorna [{ ref, branch, label, dateIso }].
92
+ */
93
+ function listGosStashes(root) {
94
+ const out = gitCapture(['stash', 'list', '--format=%gd|%gs|%ci'], { cwd: root });
95
+ if (!out) return [];
96
+ return out.split(/\r?\n/).filter(Boolean).map(line => {
97
+ const [ref, subject, dateIso] = line.split('|');
98
+ return { ref, subject, dateIso };
99
+ }).filter(s => s.subject && s.subject.includes(STASH_LABEL));
100
+ }
36
101
 
37
102
  // ---------------------------------------------------------------------------
38
103
  // Utilitarios
@@ -347,57 +412,219 @@ function cmdInstall(args) {
347
412
 
348
413
  function cmdUpdate(root, args) {
349
414
  const skipStash = args.includes('--no-stash');
415
+ const branch = resolveUpstreamBranch(root);
416
+ const mode = detectMode(root);
350
417
 
351
418
  log('Atualizando workspace ganbatte-os...');
352
419
 
353
- // 1. Verificar remote upstream
354
- const remotes = gitCapture(['remote'], { cwd: root });
355
- if (!remotes.includes(UPSTREAM_REMOTE)) {
356
- fail(`Remote "${UPSTREAM_REMOTE}" nao encontrado.`);
357
- info(`Adicione com: git remote add ${UPSTREAM_REMOTE} https://github.com/adrianomorais-ganbatte/ganbatte-os.git`);
420
+ // 0. Modo: bloquear se for projeto consumidor (não fork do framework).
421
+ if (mode === 'consumer') {
422
+ fail('Este workspace é um projeto consumidor, não o repositório do framework G-OS.');
423
+ info('Para atualizar o framework dentro do seu projeto, use:');
424
+ info(' gos install --force');
425
+ info('Ou (se você instalou via npm install):');
426
+ info(' npm install ganbatte-os@latest');
427
+ info('');
428
+ info('`gos update` é apenas para forks/clones do repositório g-os.');
358
429
  process.exit(1);
359
430
  }
360
431
 
361
- // 2. Checar se ha mudancas locais e fazer stash
362
- const status = gitCapture(['status', '--porcelain'], { cwd: root });
363
- let didStash = false;
364
- if (status && !skipStash) {
365
- log('Mudancas locais detectadas. Fazendo stash...');
366
- git(['stash', 'push', '-m', 'gos-update-auto-stash'], { cwd: root });
367
- didStash = true;
368
- ok('Stash criado.');
432
+ // 1. Validar upstream ANTES de qualquer modificação local (stash, etc).
433
+ const upstream = validateUpstream(root);
434
+ if (!upstream.ok) {
435
+ fail(`Upstream inválido: ${upstream.error}`);
436
+ if (upstream.remoteUrl) info(`URL atual: ${upstream.remoteUrl}`);
437
+ info('Corrija com:');
438
+ info(` git remote set-url ${UPSTREAM_REMOTE} ${CORRECT_UPSTREAM_URL}`);
439
+ info(` (ou via HTTPS: ${CORRECT_UPSTREAM_URL_HTTPS})`);
440
+ info('');
441
+ info(`Depois rode: npm run gos:update`);
442
+ process.exit(1);
369
443
  }
370
444
 
371
- // 3. Fetch upstream
372
- log(`Buscando atualizacoes de ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}...`);
373
- git(['fetch', UPSTREAM_REMOTE, UPSTREAM_BRANCH], { cwd: root });
445
+ // 2. Dry-run fetch (sem stashar): se nada mudou, sai cedo.
446
+ log(`Verificando ${UPSTREAM_REMOTE}/${branch}...`);
447
+ try {
448
+ git(['fetch', UPSTREAM_REMOTE, branch], { cwd: root, quiet: true });
449
+ } catch (e) {
450
+ fail(`Fetch de ${UPSTREAM_REMOTE}/${branch} falhou.`);
451
+ info('Verifique sua conexão e credenciais SSH/HTTPS. Nada foi modificado localmente.');
452
+ process.exit(1);
453
+ }
374
454
 
375
- // 4. Checar se ha commits novos
376
455
  const behind = gitCapture(
377
- ['rev-list', '--count', `HEAD..${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}`],
456
+ ['rev-list', '--count', `HEAD..${UPSTREAM_REMOTE}/${branch}`],
378
457
  { cwd: root }
379
458
  );
380
- if (behind === '0') {
381
- ok('Workspace ja esta atualizado.');
382
- if (didStash) git(['stash', 'pop'], { cwd: root });
459
+ if (behind === '0' || behind === '') {
460
+ ok(`Workspace ja esta atualizado com ${UPSTREAM_REMOTE}/${branch}.`);
383
461
  return;
384
462
  }
385
463
 
464
+ // 3. AGORA é seguro stashar (upstream validado + commits novos confirmados).
465
+ // Importante: stash deve EXCLUIR paths do framework — eles serão sobrescritos
466
+ // pelo merge de qualquer forma, e stashar a si próprio (gos-cli.js) cria um
467
+ // loop em que o stash reverte a versão atualizada do CLI no working tree.
468
+ const status = gitCapture(['status', '--porcelain'], { cwd: root });
469
+ // Detectar se há mudanças além dos paths framework
470
+ const userModifiedLines = status
471
+ .split(/\r?\n/)
472
+ .filter(Boolean)
473
+ .filter(line => {
474
+ const file = line.slice(3); // 2-char status flag + space
475
+ // Exclui paths gerados/managed pelo framework
476
+ const FRAMEWORK_PREFIXES = [
477
+ '.gos/', '.claude/', '.qwen/', '.gemini/', '.cursor/', '.agents/',
478
+ '.kilocode/', '.antigravity/', '.opencode/', '.codex/',
479
+ ];
480
+ return !FRAMEWORK_PREFIXES.some(p => file.startsWith(p));
481
+ });
482
+ let didStash = false;
483
+ if (userModifiedLines.length > 0 && !skipStash) {
484
+ log(`Mudancas locais do usuario detectadas (${userModifiedLines.length}). Fazendo stash...`);
485
+ const stashLabel = `${STASH_LABEL} ${new Date().toISOString()}`;
486
+ // Stash com pathspec excluindo paths do framework — assim o stash captura
487
+ // apenas mudanças do usuário, e o working tree mantém a versão atualizada
488
+ // dos arquivos do framework (incluindo este próprio CLI).
489
+ const stashArgs = [
490
+ 'stash', 'push', '-m', stashLabel, '--',
491
+ ':(exclude,top).gos',
492
+ ':(exclude,top).claude',
493
+ ':(exclude,top).qwen',
494
+ ':(exclude,top).gemini',
495
+ ':(exclude,top).cursor',
496
+ ':(exclude,top).agents',
497
+ ':(exclude,top).kilocode',
498
+ ':(exclude,top).antigravity',
499
+ ':(exclude,top).opencode',
500
+ ':(exclude,top).codex',
501
+ '.',
502
+ ];
503
+ git(stashArgs, { cwd: root });
504
+ didStash = true;
505
+ ok(`Stash criado (apenas mudancas do usuario): ${stashLabel}`);
506
+ } else if (status) {
507
+ info(`Mudancas locais detectadas apenas em paths do framework — serao sobrescritas pelo merge.`);
508
+ }
509
+
386
510
  const commitBefore = gitCapture(['rev-parse', '--short', 'HEAD'], { cwd: root });
387
511
 
388
512
  // 5. Merge
389
513
  log(`${behind} commit(s) novo(s). Fazendo merge...`);
390
514
  const manifest = getManifest(root);
391
515
  const frameworkPaths = manifest.frameworkManaged || [];
516
+ const allowUnrelated = args.includes('--allow-unrelated');
517
+ const clobberUntracked = args.includes('--clobber-untracked');
518
+
519
+ // Paths que podemos clobberar com segurança:
520
+ // - IDE adapters: regenerados por sync:ides
521
+ // - .gos/: framework directory, sempre vem do upstream
522
+ const FRAMEWORK_GENERATED_PREFIXES = [
523
+ '.claude/', '.qwen/', '.gemini/', '.cursor/', '.agents/',
524
+ '.kilocode/', '.antigravity/', '.opencode/', '.codex/',
525
+ '.gos/',
526
+ ];
527
+ const isGenerated = (p) => FRAMEWORK_GENERATED_PREFIXES.some(prefix => p.startsWith(prefix));
528
+
529
+ const mergeArgs = ['merge', `${UPSTREAM_REMOTE}/${branch}`, '--no-edit'];
530
+ if (allowUnrelated) mergeArgs.push('--allow-unrelated-histories');
531
+
532
+ // Função tentando o merge, capturando stderr — chamada mais de uma vez se houver retry
533
+ function tryMerge() {
534
+ try {
535
+ execFileSync('git', mergeArgs, { cwd: root, stdio: 'pipe', encoding: 'utf8' });
536
+ return { ok: true };
537
+ } catch (e) {
538
+ return { ok: false, err: ((e.stderr || '') + (e.stdout || '')).toString() };
539
+ }
540
+ }
392
541
 
393
- try {
394
- git(['merge', `${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}`, '--no-edit'], { cwd: root, quiet: true });
542
+ let mergeErr = '';
543
+ let attempt = tryMerge();
544
+ if (attempt.ok) {
395
545
  ok('Merge concluido sem conflitos.');
396
- } catch {
397
- // Checar conflitos
546
+ } else {
547
+ mergeErr = attempt.err;
548
+
549
+ // Auto-resolução: untracked files que upstream quer escrever, mas só se
550
+ // todos forem paths gerados pelo framework (sync:ides regenera).
551
+ const untrackedMatch = mergeErr.match(/untracked working tree files would be overwritten by merge:\s*([\s\S]+?)(?:Please move or remove|$)/i);
552
+ if (untrackedMatch) {
553
+ const conflictingPaths = untrackedMatch[1]
554
+ .split(/\r?\n/)
555
+ .map(s => s.trim())
556
+ .filter(Boolean)
557
+ // git termina com "Aborting", "Merge with strategy ort failed.", "Please move or remove..."
558
+ // Caminhos reais sempre contêm "." (extensão ou dotfile) OU "/" (subdir).
559
+ .filter(s => (s.includes('.') || s.includes('/')) && !/^(Aborting|Merge with|Please move)/i.test(s));
560
+ const generated = conflictingPaths.filter(isGenerated);
561
+ const userOwned = conflictingPaths.filter(p => !isGenerated(p));
562
+
563
+ if (clobberUntracked && userOwned.length === 0) {
564
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
565
+ log(`${generated.length} arquivo(s) untracked em paths regenerados — movendo para .bak.${ts}/`);
566
+ for (const file of generated) {
567
+ const src = path.join(root, file);
568
+ const dst = path.join(root, `${file}.bak.${ts}`);
569
+ if (pathExists(src)) {
570
+ ensureDir(path.dirname(dst));
571
+ fs.renameSync(src, dst);
572
+ }
573
+ }
574
+ ok(`Movidos ${generated.length} arquivo(s). Retentando merge...`);
575
+ attempt = tryMerge();
576
+ if (attempt.ok) {
577
+ ok('Merge concluido apos clobber.');
578
+ mergeErr = '';
579
+ } else {
580
+ mergeErr = attempt.err;
581
+ }
582
+ } else if (userOwned.length === 0 && !clobberUntracked) {
583
+ fail(`Merge abortou: ${conflictingPaths.length} arquivo(s) untracked seriam sobrescritos.`);
584
+ info('Todos os arquivos afetados estão em paths gerados pelo framework (regenerados por sync:ides):');
585
+ for (const p of generated.slice(0, 10)) info(` - ${p}`);
586
+ if (generated.length > 10) info(` ... mais ${generated.length - 10}`);
587
+ info('');
588
+ info('Para auto-mover esses arquivos para .bak.<timestamp> antes do merge:');
589
+ info(` npm run gos:update -- --clobber-untracked${allowUnrelated ? ' --allow-unrelated' : ''}`);
590
+ if (didStash) info('Stash preservado. Rode: git stash pop quando terminar.');
591
+ process.exit(1);
592
+ } else {
593
+ // Misto — tem arquivos do usuário também → não clobberamos nada
594
+ fail(`Merge abortou: arquivos untracked seriam sobrescritos.`);
595
+ info(`${userOwned.length} arquivo(s) NÃO gerados pelo framework (revisar antes):`);
596
+ for (const p of userOwned.slice(0, 10)) info(` - ${p}`);
597
+ info('Mova ou versione esses arquivos manualmente; depois retente.');
598
+ if (didStash) info('Stash preservado.');
599
+ process.exit(1);
600
+ }
601
+ }
602
+ }
603
+
604
+ if (!attempt.ok) {
605
+
606
+ // Detecta histórias não relacionadas (comum em workspaces criados via `gos install`)
607
+ if (/unrelated histories/i.test(mergeErr) && !allowUnrelated) {
608
+ fail('Merge abortou: histórias não relacionadas entre HEAD e upstream.');
609
+ info('Isto é comum quando o workspace foi bootstrappado via `gos install`');
610
+ info('e nunca compartilhou commits com o repositório do framework.');
611
+ info('');
612
+ info('Para forçar o merge unindo as histórias (recomendado neste caso):');
613
+ info(` npm run gos:update -- --allow-unrelated`);
614
+ info('');
615
+ info('Alternativa segura (sobrescreve apenas .gos/ preservando seus arquivos):');
616
+ info(' gos install --force');
617
+ if (didStash) info('Stash preservado. Rode: git stash pop quando resolver.');
618
+ process.exit(1);
619
+ }
620
+
621
+ // Checar conflitos de arquivo
398
622
  const conflictFiles = gitCapture(['diff', '--name-only', '--diff-filter=U'], { cwd: root });
399
623
  if (!conflictFiles) {
400
- fail('Merge falhou por motivo desconhecido.');
624
+ fail('Merge falhou. Erro do git:');
625
+ for (const line of mergeErr.split(/\r?\n/).filter(Boolean).slice(0, 6)) {
626
+ console.log(` ${line}`);
627
+ }
401
628
  if (didStash) info('Stash preservado. Rode: git stash pop');
402
629
  process.exit(1);
403
630
  }
@@ -600,12 +827,46 @@ function cmdDoctor(root) {
600
827
  check(`Diretorio ${LOCAL_DIR}/ existe`, hasLocalDir);
601
828
  }
602
829
 
603
- // 10. Report
830
+ // 10. Mode + upstream sanity (warnings, não falhas)
831
+ const mode = detectMode(root);
832
+ const branch = resolveUpstreamBranch(root);
833
+ const isCi = Boolean(process.env.CI || process.env.GITHUB_ACTIONS);
834
+ if (mode === 'framework') {
835
+ if (isCi) {
836
+ info('Upstream check ignorado em CI (sem credenciais para git ls-remote)');
837
+ } else {
838
+ const upstream = validateUpstream(root);
839
+ if (upstream.ok) {
840
+ ok(`Upstream alcançável (branch: ${branch})`);
841
+ } else {
842
+ warn(`Upstream inválido: ${upstream.error}`);
843
+ if (upstream.remoteUrl) info(`URL atual: ${upstream.remoteUrl}`);
844
+ info(`Corrija com: git remote set-url ${UPSTREAM_REMOTE} ${CORRECT_UPSTREAM_URL}`);
845
+ issues++;
846
+ }
847
+ }
848
+ checks++;
849
+ }
850
+
851
+ // 11. Stashes acumulados de updates falhos
852
+ const goshStashes = listGosStashes(root);
853
+ if (goshStashes.length > 1) {
854
+ warn(`${goshStashes.length} stashes do gos-update acumulados (sintoma de updates falhos).`);
855
+ info('Resgate com: npm run gos:rescue');
856
+ issues++;
857
+ } else if (goshStashes.length === 1) {
858
+ info(`1 stash do gos-update presente: ${goshStashes[0].ref}`);
859
+ }
860
+ checks++;
861
+
862
+ // 12. Report
604
863
  const updateLog = readJson(path.join(root, LOCAL_DIR, 'update-log.json'));
605
864
  const installLogData = readJson(path.join(root, LOCAL_DIR, 'install-log.json'));
606
865
 
607
866
  console.log('');
608
867
  console.log(` Versao: ${pkg ? pkg.version || VERSION : VERSION}`);
868
+ console.log(` Modo: ${mode === 'framework' ? 'framework workspace' : 'projeto consumidor'}`);
869
+ console.log(` Branch dev: ${branch}`);
609
870
  console.log(` Inicializado: ${installLogData ? installLogData.initializedAt : 'N/A'}`);
610
871
  console.log(` Ultimo update: ${updateLog ? updateLog.lastUpdate : 'N/A'}`);
611
872
  console.log(` Node.js: ${process.version}`);
@@ -620,6 +881,60 @@ function cmdDoctor(root) {
620
881
  process.exit(issues > 0 ? 1 : 0);
621
882
  }
622
883
 
884
+ // ---------------------------------------------------------------------------
885
+ // gos rescue — recuperar stashes acumulados de updates falhos
886
+ // ---------------------------------------------------------------------------
887
+
888
+ function cmdRescue(root, args) {
889
+ log('Buscando stashes do gos-update...');
890
+ const stashes = listGosStashes(root);
891
+ if (stashes.length === 0) {
892
+ ok('Nenhum stash do gos-update encontrado.');
893
+ return;
894
+ }
895
+
896
+ console.log('');
897
+ console.log(` ${stashes.length} stash(es) auto-criado(s) pelo gos-update:`);
898
+ console.log('');
899
+ for (const s of stashes) {
900
+ console.log(` ${s.ref} (${s.dateIso})`);
901
+ const stat = gitCapture(['stash', 'show', '--stat', s.ref], { cwd: root });
902
+ if (stat) {
903
+ for (const line of stat.split(/\r?\n/).slice(0, 5)) {
904
+ if (line.trim()) console.log(` ${line}`);
905
+ }
906
+ }
907
+ console.log('');
908
+ }
909
+
910
+ if (args.includes('--drop-all')) {
911
+ warn(`Removendo todos os ${stashes.length} stashes...`);
912
+ // Remove em ordem reversa porque os índices mudam
913
+ for (let i = stashes.length - 1; i >= 0; i--) {
914
+ git(['stash', 'drop', stashes[i].ref], { cwd: root, quiet: true });
915
+ }
916
+ ok('Todos os stashes do gos-update foram removidos.');
917
+ return;
918
+ }
919
+
920
+ if (args.includes('--pop-latest')) {
921
+ log(`Aplicando ${stashes[0].ref}...`);
922
+ try {
923
+ git(['stash', 'pop', stashes[0].ref], { cwd: root });
924
+ ok('Stash aplicado.');
925
+ } catch {
926
+ warn('Conflito ao aplicar. Resolva manualmente e rode `git stash drop` quando terminar.');
927
+ }
928
+ return;
929
+ }
930
+
931
+ info('Comandos disponíveis:');
932
+ info(' npm run gos:rescue -- --pop-latest # aplica o stash mais recente');
933
+ info(' npm run gos:rescue -- --drop-all # remove todos (após revisão)');
934
+ info(' git stash pop <ref> # aplica stash específico');
935
+ info(' git stash drop <ref> # remove stash específico');
936
+ }
937
+
623
938
  // ---------------------------------------------------------------------------
624
939
  // gos version
625
940
  // ---------------------------------------------------------------------------
@@ -627,30 +942,43 @@ function cmdDoctor(root) {
627
942
  function cmdVersion(root) {
628
943
  const pkg = readJson(path.join(root, 'package.json'), {});
629
944
  const localVersion = pkg.version || VERSION;
945
+ const mode = detectMode(root);
946
+ const branch = resolveUpstreamBranch(root);
630
947
 
631
948
  console.log(`ganbatte-os v${localVersion}`);
949
+ console.log(` modo: ${mode === 'framework' ? 'framework workspace (fork/clone do g-os)' : 'projeto consumidor'}`);
632
950
 
633
- // Checar se ha commits novos no upstream
634
- const remotes = gitCapture(['remote'], { cwd: root });
635
- if (!remotes.includes(UPSTREAM_REMOTE)) {
636
- info(`Remote "${UPSTREAM_REMOTE}" nao configurado. Nao foi possivel checar atualizacoes.`);
951
+ if (mode === 'consumer') {
952
+ info('Para checar a última versão publicada do framework:');
953
+ info(' npm view ganbatte-os version');
954
+ info('Para atualizar o framework dentro do seu projeto:');
955
+ info(' gos install --force');
956
+ return;
957
+ }
958
+
959
+ // Modo framework: checar commits novos no upstream
960
+ const upstream = validateUpstream(root);
961
+ if (!upstream.ok) {
962
+ warn(`Upstream inválido: ${upstream.error}`);
963
+ if (upstream.remoteUrl) info(`URL atual: ${upstream.remoteUrl}`);
964
+ info(`Corrija com: git remote set-url ${UPSTREAM_REMOTE} ${CORRECT_UPSTREAM_URL}`);
637
965
  return;
638
966
  }
639
967
 
640
968
  try {
641
- execFileSync('git', ['fetch', UPSTREAM_REMOTE, UPSTREAM_BRANCH], {
969
+ execFileSync('git', ['fetch', UPSTREAM_REMOTE, branch], {
642
970
  encoding: 'utf8', stdio: 'pipe', cwd: root
643
971
  });
644
972
  const behind = gitCapture(
645
- ['rev-list', '--count', `HEAD..${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}`],
973
+ ['rev-list', '--count', `HEAD..${UPSTREAM_REMOTE}/${branch}`],
646
974
  { cwd: root }
647
975
  );
648
976
 
649
977
  if (behind && behind !== '0') {
650
- console.log(`\n ${behind} commit(s) novo(s) disponivel(is).`);
978
+ console.log(`\n ${behind} commit(s) novo(s) em ${UPSTREAM_REMOTE}/${branch}.`);
651
979
  console.log(' Atualize com: npm run gos:update\n');
652
980
  } else {
653
- ok('Voce esta na versao mais recente.');
981
+ ok(`Voce esta na versao mais recente de ${UPSTREAM_REMOTE}/${branch}.`);
654
982
  }
655
983
  } catch {
656
984
  warn('Nao foi possivel checar atualizacoes (sem conexao?).');
@@ -683,6 +1011,10 @@ function main() {
683
1011
  cmdDoctor(root);
684
1012
  break;
685
1013
 
1014
+ case 'rescue':
1015
+ cmdRescue(root, args.slice(1));
1016
+ break;
1017
+
686
1018
  case 'version':
687
1019
  case '-v':
688
1020
  case '--version':
@@ -698,14 +1030,24 @@ ganbatte-os CLI v${VERSION}
698
1030
  Comandos:
699
1031
  gos install Instalar G-OS no diretorio atual (via npx ou global)
700
1032
  gos init Setup pos-clone (remote, dirs, IDEs)
701
- gos update Atualizar do upstream/main
1033
+ gos update Atualizar do upstream (apenas em fork/clone do g-os)
1034
+ gos rescue Listar/recuperar stashes acumulados de updates falhos
702
1035
  gos doctor Validar integridade do workspace
703
- gos version Mostrar versao e checar atualizacoes
1036
+ gos version Mostrar versao, modo (framework/consumer) e atualizacoes
704
1037
  gos help Exibir esta ajuda
705
1038
 
706
1039
  Flags:
707
- --force Sobrescrever arquivos existentes (install/init)
708
- --no-stash Nao fazer stash automatico (update)
1040
+ --force Sobrescrever arquivos existentes (install/init)
1041
+ --no-stash Nao fazer stash automatico (update)
1042
+ --allow-unrelated Permitir merge de histórias não relacionadas (update)
1043
+ --clobber-untracked Mover untracked files (em paths regenerados) para
1044
+ .bak.<timestamp> antes do merge (update)
1045
+ --pop-latest Aplicar stash mais recente (rescue)
1046
+ --drop-all Remover todos os stashes do gos-update (rescue)
1047
+
1048
+ Env:
1049
+ GOS_UPSTREAM_BRANCH Override da branch a ser pulled (default lê de
1050
+ .gos/config.json#defaultBranches.development)
709
1051
 
710
1052
  Exemplos:
711
1053
  npx -p ganbatte-os gos install
@@ -103,13 +103,23 @@ function main() {
103
103
  const agentTarget = relativeTarget(qwenAgent, agentProfilePath);
104
104
  const safeDesc = agentDesc.replace(/"/g, '\\"').replace(/\n/g, ' ').slice(0, 200);
105
105
  writeFile(qwenAgent, `---\nname: "gos-${agent.id}"\ndescription: "${safeDesc}"\nmodel: inherit\ntools:\n - Read\n - Glob\n - Grep\n - Bash\n - Edit\n - Write\n---\n\nFonte canonica: \`${agentTarget}\`\nLeia e siga o perfil em \`${agentTarget}\`.`);
106
+
107
+ // Codex commands (slash commands no Codex IDE Extension)
108
+ const codexCmd = path.join(root, '.codex', 'commands', 'gos', 'agents', `${agent.id}.md`);
109
+ writeFile(codexCmd, claudeCommandWrapper(`gos-${agent.id}`, agentDesc, relativeTarget(codexCmd, agentProfilePath)));
110
+
111
+ // Codex sub-agents (Codex IDE espera .codex/agents/<id>.md)
112
+ const codexAgent = path.join(root, '.codex', 'agents', `gos-${agent.id}.md`);
113
+ const codexAgentTarget = relativeTarget(codexAgent, agentProfilePath);
114
+ writeFile(codexAgent, `---\nname: "gos-${agent.id}"\ndescription: "${safeDesc}"\nmodel: inherit\ntools:\n - Read\n - Glob\n - Grep\n - Bash\n - Edit\n - Write\n---\n\nFonte canonica: \`${codexAgentTarget}\`\nLeia e siga o perfil em \`${codexAgentTarget}\`.`);
106
115
  }
107
116
 
108
117
  for (const skill of skills) {
109
118
  const skillTargetPath = skill.skillFile || skill.path;
110
119
  const canonicalPath = path.join(root, '.gos', skillTargetPath);
111
120
  const claudeSkill = path.join(root, '.claude', 'commands', 'gos', 'skills', `${skill.slug}.md`);
112
- const codexSkill = path.join(root, '.agents', 'skills', `gos-${skill.slug}`, 'SKILL.md');
121
+ const codexSkill = path.join(root, '.codex', 'skills', `gos-${skill.slug}`, 'SKILL.md');
122
+ const codexSkillCmd = path.join(root, '.codex', 'commands', 'gos', 'skills', `${skill.slug}.md`);
113
123
  const antigravitySkill = path.join(root, '.antigravity', 'skills', `gos-${skill.slug}`, 'SKILL.md');
114
124
  const geminiSkill = path.join(root, '.gemini', 'skills', `gos-${skill.slug}`, 'SKILL.md');
115
125
  const opencodeSkill = path.join(root, '.opencode', 'skills', `gos-${skill.slug}`, 'SKILL.md');
@@ -129,6 +139,7 @@ function main() {
129
139
  writeFile(qwenSkill, skillWrapper(skill.slug, relativeTarget(qwenSkill, canonicalPath), skillDesc));
130
140
 
131
141
  writeFile(qwenCmd, qwenCommandWrapper(`gos-${skill.slug}`, skillDesc, relativeTarget(qwenCmd, canonicalPath)));
142
+ writeFile(codexSkillCmd, claudeCommandWrapper(`gos-${skill.slug}`, skillDesc, relativeTarget(codexSkillCmd, canonicalPath), skillArgHint));
132
143
  writeFile(claudeSkill, claudeCommandWrapper(`gos-${skill.slug}`, skillDesc, relativeTarget(claudeSkill, canonicalPath), skillArgHint));
133
144
  }
134
145
 
@@ -169,7 +180,98 @@ function main() {
169
180
  )
170
181
  );
171
182
 
183
+ // Codex IDE Extension — AGENTS.md + config.toml
184
+ // Codex e o ambiente de EXECUCAO (Opus planeja, Codex executa). Bloco abaixo garante
185
+ // que slash commands e subagents estao disponiveis ao abrir o projeto no Codex.
186
+ const codexAgentsMd = [
187
+ '# G-OS no Codex IDE Extension',
188
+ '',
189
+ 'Este arquivo e auto-gerado por `npm run sync:ides`. Nao edite a mao.',
190
+ '',
191
+ 'Codex IDE Extension e o ambiente de EXECUCAO do G-OS. Opus 4.7 planeja em outra IDE/sessao;',
192
+ 'Codex executa task-a-task com `*execute-plan`.',
193
+ '',
194
+ 'Leia sempre:',
195
+ '- `../AGENTS.md` (raiz do projeto)',
196
+ '- `../CLAUDE.md`',
197
+ '- `../.gos/docs/toolchain-map.md`',
198
+ '',
199
+ '## Execucao de planos (comando primario do Codex)',
200
+ '',
201
+ '```',
202
+ '*execute-plan PLAN-NNN-<slug>',
203
+ '```',
204
+ '',
205
+ 'Ciclo: pre-flight visual -> loop por task com state machine -> visual gate -> validacao -> humano marca concluido.',
206
+ 'Detalhes: `../.gos/skills/execute-plan/SKILL.md`.',
207
+ '',
208
+ '## Agents disponiveis',
209
+ '',
210
+ ...agents.map((agent) => `- \`gos-${agent.id}\` -> \`../.gos/agents/profiles/${agent.path}\``),
211
+ '',
212
+ '## Skills curadas',
213
+ '',
214
+ '| Slug | Arquivo canonico |',
215
+ '|------|------------------|',
216
+ ...skills.map((skill) => `| \`gos-${skill.slug}\` | \`../.gos/${skill.skillFile || skill.path}\` |`),
217
+ '',
218
+ '## Como o Codex consome',
219
+ '',
220
+ '- Slash commands em `.codex/commands/gos/{agents,skills}/<id>.md` -> Codex carrega o canonico apontado em CANONICAL-SOURCE e executa.',
221
+ '- Subagents em `.codex/agents/gos-<id>.md` -> referencia o profile em `.gos/agents/profiles/`.',
222
+ '- Skills em `.codex/skills/gos-<slug>/SKILL.md` -> wrapper fino que aponta para `.gos/skills/<slug>/SKILL.md`.',
223
+ ''
224
+ ].join('\n');
225
+ writeFile(path.join(root, '.codex', 'AGENTS.md'), codexAgentsMd);
226
+
227
+ const codexConfigToml = [
228
+ '# G-OS Codex IDE Extension config (auto-gerado por npm run sync:ides).',
229
+ '# Edite os arquivos canonicos em .gos/ ao inves deste.',
230
+ '',
231
+ 'project = "g-os"',
232
+ '',
233
+ '[instructions]',
234
+ 'files = [',
235
+ ' "AGENTS.md",',
236
+ ' "../AGENTS.md",',
237
+ ' "../CLAUDE.md",',
238
+ ' "../.gos/docs/toolchain-map.md",',
239
+ ']',
240
+ '',
241
+ '[execution]',
242
+ 'primary_command = "*execute-plan"',
243
+ 'planning_command = "*plan"',
244
+ 'progress_command = "*progress"',
245
+ 'stack_command = "*stack"',
246
+ ''
247
+ ].join('\n');
248
+ writeFile(path.join(root, '.codex', 'config.toml'), codexConfigToml);
249
+
250
+ // Validacao final: garantir que os arquivos do Codex foram gerados.
251
+ // Evita regressoes silenciosas que ja quebraram a IDE no passado.
252
+ const codexFailures = [];
253
+ for (const agent of agents) {
254
+ const expectedAgent = path.join(root, '.codex', 'agents', `gos-${agent.id}.md`);
255
+ const expectedCmd = path.join(root, '.codex', 'commands', 'gos', 'agents', `${agent.id}.md`);
256
+ if (!fs.existsSync(expectedAgent)) codexFailures.push(expectedAgent);
257
+ if (!fs.existsSync(expectedCmd)) codexFailures.push(expectedCmd);
258
+ }
259
+ for (const skill of skills) {
260
+ const expectedSkill = path.join(root, '.codex', 'skills', `gos-${skill.slug}`, 'SKILL.md');
261
+ const expectedCmd = path.join(root, '.codex', 'commands', 'gos', 'skills', `${skill.slug}.md`);
262
+ if (!fs.existsSync(expectedSkill)) codexFailures.push(expectedSkill);
263
+ if (!fs.existsSync(expectedCmd)) codexFailures.push(expectedCmd);
264
+ }
265
+ if (!fs.existsSync(path.join(root, '.codex', 'AGENTS.md'))) codexFailures.push('.codex/AGENTS.md');
266
+ if (!fs.existsSync(path.join(root, '.codex', 'config.toml'))) codexFailures.push('.codex/config.toml');
267
+ if (codexFailures.length > 0) {
268
+ console.error(`[sync:ides] Codex IDE adapters incompletos. Faltando ${codexFailures.length} arquivos:`);
269
+ for (const f of codexFailures) console.error(` - ${path.relative(root, f)}`);
270
+ process.exit(1);
271
+ }
272
+
172
273
  console.log(`Adapters generated for ${agents.length} agents and ${skills.length} skills.`);
274
+ console.log(`Codex IDE: ${agents.length} agents + ${skills.length} skills + AGENTS.md + config.toml.`);
173
275
  }
174
276
 
175
277
  main();