g360-cli 1.2.2 → 1.4.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.
Files changed (30) hide show
  1. package/README.md +182 -3
  2. package/dist/g360.exe +0 -0
  3. package/package.json +8 -8
  4. package/src/assets/config/g360-skills.json +52 -0
  5. package/src/assets/config/project-types.json +16 -0
  6. package/src/assets/config/skills.json +12 -0
  7. package/src/assets/snippets/snippets.json +77 -0
  8. package/src/assets/templates/python-cli/build-portable.bat +33 -0
  9. package/src/assets/templates/python-cli/src/main.py +107 -3
  10. package/src/assets/templates/python-customtkinter/README.md +47 -0
  11. package/src/assets/templates/python-customtkinter/build-portable.bat +28 -0
  12. package/src/assets/templates/python-customtkinter/requirements.txt +1 -0
  13. package/src/assets/templates/python-customtkinter/run.bat +29 -0
  14. package/src/assets/templates/python-customtkinter/src/core/skill.json +22 -0
  15. package/src/assets/templates/python-customtkinter/src/main.py +123 -0
  16. package/src/assets/templates/python-flet/README.md +130 -0
  17. package/src/assets/templates/python-flet/build-portable.bat +42 -0
  18. package/src/assets/templates/python-flet/create_shortcut.vbs +35 -0
  19. package/src/assets/templates/python-flet/requirements.txt +3 -0
  20. package/src/assets/templates/python-flet/run.bat +81 -0
  21. package/src/assets/templates/python-flet/src/core/skill.json +21 -0
  22. package/src/assets/templates/python-flet/src/main.py +101 -0
  23. package/src/assets/templates/python-flet/src/test_app.py +103 -0
  24. package/src/assets/templates/python-flet-migrate/src/main.py +68 -0
  25. package/src/assets/templates/python-flet-migrate/src/migrate_tkinter.py +56 -0
  26. package/src/cli.js +12 -2
  27. package/src/commands/clean.js +51 -4
  28. package/src/commands/init.js +92 -8
  29. package/src/commands/signature.js +12 -8
  30. package/src/lib/auditor.js +39 -2
@@ -0,0 +1,103 @@
1
+ """
2
+ Tests para la aplicación G360 Flet
3
+ Usa: pytest test_app.py -v
4
+ """
5
+
6
+ import unittest
7
+ from unittest.mock import Mock, patch
8
+ import flet as ft
9
+
10
+
11
+ class TestG360App(unittest.TestCase):
12
+ """Tests básicos para la aplicación Flet"""
13
+
14
+ def setUp(self):
15
+ self.mock_page = Mock(spec=ft.Page)
16
+
17
+ def test_page_theme_setup(self):
18
+ """Verifica que la página se configura con el tema correcto"""
19
+ self.mock_page.title = "G360 Desktop App"
20
+ self.mock_page.theme_mode = ft.ThemeMode.DARK
21
+ self.mock_page.bgcolor = "#0b1220"
22
+
23
+ self.assertEqual(self.mock_page.title, "G360 Desktop App")
24
+ self.assertEqual(self.mock_page.theme_mode, ft.ThemeMode.DARK)
25
+ self.assertEqual(self.mock_page.bgcolor, "#0b1220")
26
+
27
+ def test_colors_g360(self):
28
+ """Verifica los colores del tema G360"""
29
+ colors = {
30
+ "bg": "#0b1220",
31
+ "surface": "#151e2e",
32
+ "accent": "#00d084",
33
+ "text": "#f0f4f8",
34
+ "muted": "#94a3b8"
35
+ }
36
+
37
+ for key, value in colors.items():
38
+ self.assertIsNotNone(value)
39
+
40
+ def test_button_styles(self):
41
+ """Verifica estilos de botones G360"""
42
+ button = ft.ElevatedButton(
43
+ text="Test",
44
+ bgcolor="#00d084",
45
+ color="#0b1220"
46
+ )
47
+
48
+ self.assertIsNotNone(button)
49
+
50
+ def test_container_styles(self):
51
+ """Verifica estilos de contenedores G360"""
52
+ container = ft.Container(
53
+ bgcolor="#151e2e",
54
+ border_radius=12,
55
+ padding=20
56
+ )
57
+
58
+ self.assertIsNotNone(container)
59
+ self.assertEqual(container.bgcolor, "#151e2e")
60
+ self.assertEqual(container.border_radius, 12)
61
+
62
+
63
+ class TestMigration(unittest.TestCase):
64
+ """Tests para migración desde tkinter/ctkinter"""
65
+
66
+ def test_convert_tkinter_widgets(self):
67
+ """Verifica mapeo de widgets tkinter a Flet"""
68
+ mapping = {
69
+ 'tk.Label': 'ft.Text',
70
+ 'tk.Button': 'ft.ElevatedButton',
71
+ 'tk.Entry': 'ft.TextField',
72
+ 'tk.Frame': 'ft.Container',
73
+ }
74
+
75
+ for tk, flet in mapping.items():
76
+ self.assertIsNotNone(tk)
77
+ self.assertIsNotNone(flet)
78
+
79
+
80
+ class TestCalculations(unittest.TestCase):
81
+ """Tests para cálculos y procesamiento de datos"""
82
+
83
+ def test_sum_basic(self):
84
+ """Test básico de suma"""
85
+ result = 2 + 2
86
+ self.assertEqual(result, 4)
87
+
88
+ def test_list_operations(self):
89
+ """Test operaciones con listas"""
90
+ items = [1, 2, 3, 4, 5]
91
+ self.assertEqual(sum(items), 15)
92
+ self.assertEqual(len(items), 5)
93
+ self.assertEqual(max(items), 5)
94
+
95
+ def test_dict_operations(self):
96
+ """Test operaciones con diccionarios"""
97
+ data = {"sku": "ABC001", "cantidad": 10, "precio": 50.0}
98
+ self.assertIn("sku", data)
99
+ self.assertEqual(data["cantidad"] * data["precio"], 500.0)
100
+
101
+
102
+ if __name__ == '__main__':
103
+ unittest.main()
@@ -0,0 +1,68 @@
1
+ import flet as ft
2
+
3
+
4
+ class MigratedApp:
5
+ def __init__(self, page: ft.Page):
6
+ self.page = page
7
+ self.setup_theme()
8
+ self.create_ui()
9
+
10
+ def setup_theme(self):
11
+ self.page.title = "G360 - Migrated App"
12
+ self.page.theme_mode = ft.ThemeMode.DARK
13
+ self.page.padding = 20
14
+ self.page.bgcolor = "#0b1220"
15
+
16
+ def create_ui(self):
17
+ self.page.add(
18
+ ft.Column([
19
+ self.create_header(),
20
+ self.create_main_content(),
21
+ ], spacing=20, expand=True)
22
+ )
23
+
24
+ def create_header(self):
25
+ return ft.Container(
26
+ content=ft.Row([
27
+ ft.Text("G360 Migrated App", size=28, weight=ft.FontWeight.BOLD, color="#00d084"),
28
+ ft.Container(expand=True),
29
+ ft.Text("from tkinter/ctkinter", size=14, color="#94a3b8"),
30
+ ]),
31
+ padding=20,
32
+ bgcolor="#151e2e",
33
+ border_radius=12
34
+ )
35
+
36
+ def create_main_content(self):
37
+ return ft.Container(
38
+ content=ft.Column([
39
+ ft.Text("Panel de Controles (antes tk.Button)", size=20, color="#f0f4f8"),
40
+ ft.Row([
41
+ ft.ElevatedButton("Aceptar", bgcolor="#00d084", color="#0b1220"),
42
+ ft.ElevatedButton("Cancelar", bgcolor="#ef4444", color="#ffffff"),
43
+ ft.ElevatedButton("Guardar", bgcolor="#00796B", color="#ffffff"),
44
+ ], spacing=15),
45
+ ft.Container(height=30),
46
+ ft.Text("Campos de Entrada (antes tk.Entry)", size=20, color="#f0f4f8"),
47
+ ft.Column([
48
+ ft.TextField(label="Usuario", hint_text="Ingrese usuario", width=300),
49
+ ft.TextField(label="Contraseña", hint_text="Ingrese contraseña", password=True, width=300),
50
+ ], spacing=15),
51
+ ft.Container(height=30),
52
+ ft.Text("Casillas y Opciones (antes tk.Checkbutton/tk.Radiobutton)", size=20, color="#f0f4f8"),
53
+ ft.Column([
54
+ ft.Checkbox(label="Recordarme"),
55
+ ft.Checkbox(label="Aceptar términos"),
56
+ ], spacing=10),
57
+ ], scroll=ft.ScrollMode.AUTO),
58
+ expand=True,
59
+ padding=20
60
+ )
61
+
62
+
63
+ def main(page: ft.Page):
64
+ MigratedApp(page)
65
+
66
+
67
+ if __name__ == "__main__":
68
+ ft.app(target=main)
@@ -0,0 +1,56 @@
1
+ """
2
+ Migración de tkinter/ctkinter a Flet
3
+ Script de ayuda para convertir código existente
4
+ """
5
+
6
+ MAPPING = {
7
+ 'tk.Label': 'ft.Text',
8
+ 'tk.Button': 'ft.ElevatedButton',
9
+ 'tk.Entry': 'ft.TextField',
10
+ 'tk.Checkbutton': 'ft.Checkbox',
11
+ 'tk.Radiobutton': 'ft.Radio',
12
+ 'tk.Listbox': 'ft.ListView',
13
+ 'tk.Frame': 'ft.Container',
14
+ 'tk.LabelFrame': 'ft.Container with border',
15
+ 'tk.Canvas': 'ft.Canvas',
16
+ 'tk.Menu': 'ft.AppBar with actions',
17
+ 'ctk.CTkButton': 'ft.ElevatedButton',
18
+ 'ctk.CTkLabel': 'ft.Text',
19
+ 'ctk.CTkEntry': 'ft.TextField',
20
+ 'ctk.CTkFrame': 'ft.Container',
21
+ 'ctk.CTkCheckBox': 'ft.Checkbox',
22
+ 'ctk.CTkRadioButton': 'ft.Radio',
23
+ 'ctk.CTkProgressBar': 'ft.ProgressBar',
24
+ 'ctk.CTkSlider': 'ft.Slider',
25
+ 'ctk.CTkSwitch': 'ft.Switch',
26
+ 'ctk.CTkComboBox': 'ft.Dropdown',
27
+ 'ctk.CTkTextbox': 'ft.TextField multiline',
28
+ 'ctk.CTkTabview': 'ft.Tabs',
29
+ }
30
+
31
+ LAYOUT_MAPPING = {
32
+ 'pack()': 'page.add()',
33
+ 'pack(fill=..., expand=...)': 'expand=True, alignment=...',
34
+ 'grid(row=..., column=...)': 'use Column with Row for grid-like',
35
+ 'place(x=..., y=...)': 'position in Container',
36
+ }
37
+
38
+ THEME_MAPPING = {
39
+ 'bg': 'bgcolor',
40
+ 'fg': 'color',
41
+ 'font': 'size + weight',
42
+ 'relief': 'border_style',
43
+ 'padx/pady': 'padding',
44
+ 'width': 'width',
45
+ 'height': 'height',
46
+ }
47
+
48
+ def convert_tkinter_to_flet(code: str) -> str:
49
+ result = code
50
+ for tk_widget, flet_widget in MAPPING.items():
51
+ result = result.replace(tk_widget, flet_widget)
52
+ return result
53
+
54
+ print("Mapeo de tkinter/ctkinter a Flet cargado")
55
+ print("Usa: from migrate_tkinter import convert_tkinter_to_flet")
56
+ print("Para convertir código existente a Flet")
package/src/cli.js CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  import { Command } from 'commander';
4
4
  import chalk from 'chalk';
5
+ import fs from 'fs-extra';
6
+ import path from 'path';
7
+ import { fileURLToPath } from 'url';
5
8
  import { init } from './commands/init.js';
6
9
  import { setSkill } from './commands/set-skill.js';
7
10
  import { bring } from './commands/bring.js';
@@ -14,21 +17,26 @@ import { update } from './commands/update.js';
14
17
  import { convert } from './commands/convert.js';
15
18
  import { signature } from './commands/signature.js';
16
19
 
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ const pkg = fs.readJsonSync(path.join(__dirname, '../package.json'));
22
+
17
23
  const program = new Command();
18
24
 
19
25
  program
20
26
  .name('g360')
21
27
  .description('CLI tool for bootstrapping G360 ecosystem projects')
22
- .version('1.2.2');
28
+ .version(pkg.version);
23
29
 
24
30
  program
25
31
  .command('init')
26
32
  .argument('<name>', 'Project name')
27
33
  .option('-t, --template <type>', 'Project template type', 'web-pwa')
28
- .option('-s, --skill <skill>', 'Skill to use (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom)', 'corporativo-movil')
34
+ .option('-s, --skill <skill>', 'Skill to use (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom, flet-desktop)', 'corporativo-movil')
29
35
  .option('-d, --dir <path>', 'Target directory', '.')
30
36
  .option('--dry-run', 'Preview without creating files')
31
37
  .option('--force', 'Overwrite existing files')
38
+ .option('--portable', 'Create portable version (for python-flet, python-cli)')
39
+ .option('--no-portable', 'Skip portable version creation')
32
40
  .action(init);
33
41
 
34
42
  program
@@ -75,6 +83,8 @@ program
75
83
  .option('--orphans', 'Remove orphan files (unreferenced)')
76
84
  .option('--organize', 'Show misplaced files suggestions')
77
85
  .option('--all', 'Run all cleanup tasks')
86
+ .option('--github', 'Prepare repo for GitHub (update .gitignore)')
87
+ .option('--pre-push', 'Alias for --github (prepare before push)')
78
88
  .action(clean);
79
89
 
80
90
  program
@@ -21,14 +21,16 @@ const DEAD_PATTERNS = [
21
21
  const DUPLICATE_EXTENSIONS = ['.bak', '.orig', '.swp', '.swo', '~'];
22
22
 
23
23
  export async function clean(projectPath, options) {
24
- const {
25
- dryRun = false,
24
+ const {
25
+ dryRun = false,
26
26
  force = false,
27
27
  dead = false,
28
28
  duplicates = false,
29
29
  orphans = false,
30
30
  organize = false,
31
- all = false
31
+ all = false,
32
+ github = false,
33
+ prePush = false
32
34
  } = options;
33
35
 
34
36
  const targetDir = path.join(process.cwd(), projectPath);
@@ -123,8 +125,53 @@ export async function clean(projectPath, options) {
123
125
  return;
124
126
  }
125
127
 
128
+ if (github || prePush) {
129
+ console.log(chalk.bold.cyan('\n📦 Modo Pre-Push GitHub\n'));
130
+ console.log(chalk.gray('Limpiando para subir a repositorio remoto...\n'));
131
+
132
+ const gitIgnore = path.join(targetDir, '.gitignore');
133
+ const requiredIgnores = [
134
+ 'node_modules/',
135
+ '__pycache__/',
136
+ '*.pyc',
137
+ '.env',
138
+ 'dist/',
139
+ 'build/',
140
+ '*.log',
141
+ '.cache/',
142
+ 'coverage/',
143
+ '*.egg-info/',
144
+ '.pytest_cache/'
145
+ ];
146
+
147
+ if (fs.existsSync(gitIgnore)) {
148
+ const currentIgnore = await fs.readFile(gitIgnore, 'utf-8');
149
+ let updatedIgnore = currentIgnore;
150
+
151
+ for (const pattern of requiredIgnores) {
152
+ if (!currentIgnore.includes(pattern)) {
153
+ updatedIgnore += `\n${pattern}`;
154
+ }
155
+ }
156
+
157
+ await fs.writeFile(gitIgnore, updatedIgnore);
158
+ console.log(chalk.green(' ✅ .gitignore actualizado'));
159
+ } else {
160
+ await fs.writeFile(gitIgnore, requiredIgnores.join('\n') + '\n');
161
+ console.log(chalk.green(' ✅ .gitignore creado'));
162
+ }
163
+
164
+ console.log(chalk.green('\n✅ Repo listo para GitHub!\n'));
165
+ console.log(chalk.gray('Para subir:'));
166
+ console.log(` ${chalk.cyan('git add .')}`);
167
+ console.log(` ${chalk.cyan('git commit -m "clean: preparing for push"')}`);
168
+ console.log(` ${chalk.cyan('git push')}\n`);
169
+
170
+ return;
171
+ }
172
+
126
173
  console.log(chalk.cyan('\n🚀 Aplicando limpieza...'));
127
-
174
+
128
175
  let cleaned = 0;
129
176
 
130
177
  for (const file of issues.dead) {
@@ -5,19 +5,61 @@ import { fileURLToPath } from 'url';
5
5
  import { manifest } from '../lib/manifest.js';
6
6
  import { progress } from '../lib/progress.js';
7
7
  import { setSkill } from './set-skill.js';
8
+ import inquirer from 'inquirer';
8
9
 
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
11
 
12
+ const PORTABLE_TEMPLATES = ['python-flet', 'python-flet-migrate', 'python-cli', 'python-customtkinter'];
13
+
14
+ async function askPortableOption(template) {
15
+ if (!PORTABLE_TEMPLATES.includes(template)) {
16
+ return false;
17
+ }
18
+
19
+ const answers = await inquirer.prompt([
20
+ {
21
+ type: 'confirm',
22
+ name: 'portable',
23
+ message: '¿Deseas crear una versión portable del proyecto? (ejecutable standalone)',
24
+ default: false
25
+ }
26
+ ]);
27
+
28
+ return answers.portable;
29
+ }
30
+
11
31
  export async function init(name, options) {
12
- const { template = 'web-pwa', skill = 'corporativo-movil', dir = '.', dryRun = false, force = false } = options;
32
+ const {
33
+ template = 'web-pwa',
34
+ skill = 'corporativo-movil',
35
+ dir = '.',
36
+ dryRun = false,
37
+ force = false,
38
+ portable = null
39
+ } = options;
40
+
13
41
  const targetDir = path.join(process.cwd(), dir, name);
14
-
42
+
15
43
  console.log(chalk.bold.cyan('\n🚀 G360 Project Initialization\n'));
16
44
  console.log(`Project: ${chalk.yellow(name)}`);
17
45
  console.log(`Template: ${chalk.blue(template)}`);
18
46
  console.log(`Skill: ${chalk.magenta(skill)}`);
19
47
  console.log(`Target: ${chalk.gray(targetDir)}\n`);
20
48
 
49
+ let wantPortable = false;
50
+
51
+ if (portable === true) {
52
+ wantPortable = true;
53
+ console.log(chalk.yellow('📦 Versión portable: SÍ (flag)\n'));
54
+ } else if (portable === false) {
55
+ console.log(chalk.gray('📦 Versión portable: NO\n'));
56
+ } else {
57
+ wantPortable = await askPortableOption(template);
58
+ if (wantPortable) {
59
+ console.log(chalk.yellow('📦 Versión portable: SÍ\n'));
60
+ }
61
+ }
62
+
21
63
  const templatesPath = path.join(__dirname, '../assets/templates');
22
64
 
23
65
  if (!fs.existsSync(templatesPath)) {
@@ -49,24 +91,66 @@ export async function init(name, options) {
49
91
  const progressBar = progress('Creating project...');
50
92
 
51
93
  try {
52
- // Asegurar que el directorio base existe
53
94
  await fs.ensureDir(path.dirname(targetDir));
54
95
 
55
96
  await fs.copy(templateDir, targetDir);
56
- await manifest.init(targetDir, { name, template, version: '1.0.0' });
57
-
58
- // Aplicar el skill pasando el directorio destino explícitamente
97
+ await manifest.init(targetDir, { name, template, version: '1.0.0', portable: wantPortable });
98
+
99
+ if (wantPortable) {
100
+ const portableDir = path.join(targetDir, 'portable');
101
+ await fs.ensureDir(portableDir);
102
+ const buildScript = path.join(targetDir, 'build-portable.bat');
103
+ if (fs.existsSync(buildScript)) {
104
+ await fs.copy(buildScript, path.join(portableDir, 'build.bat'));
105
+ }
106
+ console.log(chalk.gray(' 📦 Carpeta portable/ creada para builds\n'));
107
+ }
108
+
59
109
  await setSkill(skill, { force: true, verbose: options.verbose, cwd: targetDir });
60
110
 
111
+ await createG360Structure(targetDir, templatesPath);
112
+
61
113
  progressBar.stop();
62
-
114
+
63
115
  console.log(chalk.green('\n✅ Project created successfully!\n'));
116
+ if (wantPortable) {
117
+ console.log(chalk.yellow('📦 Versión portable habilitada\n'));
118
+ }
64
119
  console.log(chalk.gray('Next steps:'));
65
120
  console.log(` ${chalk.cyan('cd')} ${name}`);
66
121
  console.log(` ${chalk.cyan('g360 bring')}`);
67
122
  console.log(` ${chalk.cyan('g360 present')}\n`);
68
- } catch (error) {
123
+ } catch (error) {
69
124
  progressBar.stop();
70
125
  console.error(chalk.red(`\n❌ Error: ${error.message}`));
71
126
  }
72
127
  }
128
+
129
+ async function createG360Structure(projectPath, assetsDir) {
130
+ try {
131
+ const g360Dir = path.join(projectPath, 'g360');
132
+ await fs.ensureDir(g360Dir);
133
+
134
+ const subdirs = ['skills', 'snippets', 'samples', 'config', 'engine'];
135
+ for (const dir of subdirs) {
136
+ await fs.ensureDir(path.join(g360Dir, dir));
137
+ }
138
+
139
+ const skillJsonPath = path.join(g360Dir, 'skill.json');
140
+ if (!fs.existsSync(skillJsonPath)) {
141
+ const exampleSkillPath = path.join(assetsDir, 'config/skills.json');
142
+ if (fs.existsSync(exampleSkillPath)) {
143
+ await fs.copy(exampleSkillPath, skillJsonPath);
144
+ }
145
+ }
146
+
147
+ const snippetsDir = path.join(g360Dir, 'snippets');
148
+ const exampleSnippetsPath = path.join(assetsDir, 'snippets/snippets.json');
149
+ if (fs.existsSync(exampleSnippetsPath) && !fs.existsSync(path.join(snippetsDir, 'snippets.json'))) {
150
+ await fs.copy(exampleSnippetsPath, path.join(snippetsDir, 'snippets.json'));
151
+ }
152
+
153
+ } catch (error) {
154
+ console.log(chalk.yellow(`Warning: Could not create G360 structure: ${error.message}`));
155
+ }
156
+ }
@@ -21,20 +21,24 @@ export async function signature(command, options) {
21
21
 
22
22
  let htmlContent = fs.readFileSync(indexHtmlPath, 'utf8');
23
23
 
24
- // Verificar si ya esta instalado
25
- if (htmlContent.includes('g360-signature') && !force) {
26
- console.log(chalk.yellow('⚠️ g360-signature ya se encuentra instalado'));
27
- console.log(chalk.gray('Usa --force para reinstalar'));
28
- return;
29
- }
30
-
31
- // Agregar script CDN antes del cierre del body
32
24
  const scriptTag = ' <script type="module" src="https://unpkg.com/g360-signature@latest/index.js"></script>';
33
25
  const signatureComponent = `
34
26
  <!-- Firma Oficial G360 -->
35
27
  <g360-signature mode="powered" style="position: fixed; bottom: 16px; right: 16px; z-index: 99999;"></g360-signature>
36
28
  `;
37
29
 
30
+ // Si --force es true, eliminamos cualquier instancia existente para una reinstalación limpia.
31
+ // De lo contrario, si ya está instalado y no se fuerza, salimos.
32
+ if (force) {
33
+ htmlContent = htmlContent.replace(new RegExp(scriptTag.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '');
34
+ htmlContent = htmlContent.replace(new RegExp(signatureComponent.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '');
35
+ htmlContent = htmlContent.replace(/\n\s*\n/g, '\n'); // Limpiar posibles líneas vacías extra
36
+ } else if (htmlContent.includes('g360-signature')) {
37
+ console.log(chalk.yellow('⚠️ g360-signature ya se encuentra instalado'));
38
+ console.log(chalk.gray('Usa --force para reinstalar'));
39
+ return;
40
+ }
41
+
38
42
  if (!htmlContent.includes(scriptTag)) {
39
43
  htmlContent = htmlContent.replace('</body>', `${scriptTag}\n </body>`);
40
44
  }
@@ -46,7 +46,25 @@ export const auditor = {
46
46
  if (fs.existsSync(manifestPath)) {
47
47
  return { status: 'pass', details: 'Manifest found' };
48
48
  }
49
- return { status: 'warn', issue: { file: 'g360-manifest.json', message: 'No manifest found' } };
49
+
50
+ const pkgPath = path.join(projectDir, 'package.json');
51
+ if (fs.existsSync(pkgPath)) {
52
+ try {
53
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
54
+ const isG360Tool =
55
+ pkg.name?.startsWith('g360-') ||
56
+ (pkg.keywords && pkg.keywords.some(k => k.includes('g360'))) ||
57
+ pkg.name === 'g360-cli';
58
+
59
+ if (isG360Tool) {
60
+ return { status: 'pass', details: 'G360 tool/package - manifest optional' };
61
+ }
62
+ } catch (e) {
63
+ // Continue to warning
64
+ }
65
+ }
66
+
67
+ return { status: 'warn', issue: { file: 'g360-manifest.json', message: 'No manifest found - run "g360 bring" to initialize' } };
50
68
  },
51
69
 
52
70
  checkStructure(projectDir) {
@@ -54,7 +72,26 @@ export const auditor = {
54
72
  if (fs.existsSync(g360Dir)) {
55
73
  return { status: 'pass', details: 'G360 assets directory found' };
56
74
  }
57
- return { status: 'warn', issue: { file: 'g360/', message: 'No G360 assets found' } };
75
+
76
+ // Check if this is a G360 tool/package - structure optional
77
+ const pkgPath = path.join(projectDir, 'package.json');
78
+ if (fs.existsSync(pkgPath)) {
79
+ try {
80
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
81
+ const isG360Tool =
82
+ pkg.name?.startsWith('g360-') ||
83
+ (pkg.keywords && pkg.keywords.some(k => k.includes('g360'))) ||
84
+ pkg.name === 'g360-cli';
85
+
86
+ if (isG360Tool) {
87
+ return { status: 'pass', details: 'G360 tool/package - G360 structure optional' };
88
+ }
89
+ } catch (e) {
90
+ // If package.json is invalid, continue to warning
91
+ }
92
+ }
93
+
94
+ return { status: 'warn', issue: { file: 'g360/', message: 'No G360 assets found - run "g360 bring" to initialize assets' } };
58
95
  },
59
96
 
60
97
  checkConfig(projectDir) {