boxsafe 1.0.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 (118) hide show
  1. package/.directory +2 -0
  2. package/.env.example +3 -0
  3. package/AUDIT_LANG.md +45 -0
  4. package/BOXSAFE_VERSION_NOTES.md +14 -0
  5. package/README.md +4 -0
  6. package/TODO.md +130 -0
  7. package/adapters/index.ts +27 -0
  8. package/adapters/primary/cli-adapter.ts +56 -0
  9. package/adapters/secondary/filesystem/node-filesystem.ts +307 -0
  10. package/adapters/secondary/system/configuration.ts +147 -0
  11. package/ai/caller.ts +42 -0
  12. package/ai/label.ts +33 -0
  13. package/ai/modelConfig.ts +236 -0
  14. package/ai/provider.ts +111 -0
  15. package/boxsafe.config.json +68 -0
  16. package/core/auth/dasktop/cred/CRED.md +112 -0
  17. package/core/auth/dasktop/cred/credLinux.ts +82 -0
  18. package/core/auth/dasktop/cred/credWin.ts +2 -0
  19. package/core/config/defaults/boxsafeDefaults.ts +67 -0
  20. package/core/config/defaults/index.ts +1 -0
  21. package/core/config/loadConfig.ts +133 -0
  22. package/core/loop/about.md +13 -0
  23. package/core/loop/boxConfig.ts +20 -0
  24. package/core/loop/buildExecCommand.ts +76 -0
  25. package/core/loop/cmd/execode.ts +121 -0
  26. package/core/loop/cmd/test.js +3 -0
  27. package/core/loop/execLoop.ts +341 -0
  28. package/core/loop/git/VERSIONING.md +17 -0
  29. package/core/loop/git/commands.ts +11 -0
  30. package/core/loop/git/gitClient.ts +78 -0
  31. package/core/loop/git/index.ts +99 -0
  32. package/core/loop/git/runVersionControlRunner.ts +33 -0
  33. package/core/loop/initNavigator.ts +44 -0
  34. package/core/loop/initTasksManager.ts +35 -0
  35. package/core/loop/runValidation.ts +25 -0
  36. package/core/loop/tasks/AGENT-TASKS.md +36 -0
  37. package/core/loop/tasks/index.ts +96 -0
  38. package/core/loop/toolCalls.ts +168 -0
  39. package/core/loop/toolDispatcher.ts +146 -0
  40. package/core/loop/traceLogger.ts +106 -0
  41. package/core/loop/types.ts +26 -0
  42. package/core/loop/versionControlAdapter.ts +36 -0
  43. package/core/loop/waterfall.ts +404 -0
  44. package/core/loop/writeArtifactAtomically.ts +13 -0
  45. package/core/navigate/NAVIGATE.md +186 -0
  46. package/core/navigate/about.md +128 -0
  47. package/core/navigate/examples.ts +367 -0
  48. package/core/navigate/handler.ts +148 -0
  49. package/core/navigate/index.ts +32 -0
  50. package/core/navigate/navigate.test.ts +372 -0
  51. package/core/navigate/navigator.ts +437 -0
  52. package/core/navigate/types.ts +132 -0
  53. package/core/navigate/utils.ts +146 -0
  54. package/core/paths/paths.ts +33 -0
  55. package/core/ports/index.ts +271 -0
  56. package/core/segments/CONVENTIONS.md +30 -0
  57. package/core/segments/loop/index.ts +18 -0
  58. package/core/segments/map.ts +56 -0
  59. package/core/segments/navigate/index.ts +20 -0
  60. package/core/segments/versionControl/index.ts +18 -0
  61. package/core/util/logger.ts +128 -0
  62. package/docs/AGENT-TASKS.md +36 -0
  63. package/docs/ARQUITETURA_CORRECAO.md +121 -0
  64. package/docs/CONVENTIONS.md +30 -0
  65. package/docs/CRED.md +112 -0
  66. package/docs/L_RAG.md +567 -0
  67. package/docs/NAVIGATE.md +186 -0
  68. package/docs/PRIMARY_ACTORS.md +78 -0
  69. package/docs/SECONDARY_ACTORS.md +174 -0
  70. package/docs/VERSIONING.md +17 -0
  71. package/docs/boxsafe.config.md +472 -0
  72. package/eslint.config.mts +15 -0
  73. package/main.ts +53 -0
  74. package/memo/generated/codelog.md +13 -0
  75. package/memo/state/tasks/state.json +6 -0
  76. package/memo/state/tasks/tasks/task_001.md +2 -0
  77. package/memo/states-logs/logs.txt +7 -0
  78. package/memo/states-logs/trace-mljvrxvi-9g0k4q.jsonl +11 -0
  79. package/memo/states-logs/trace-mljvvc9j-pe9ekj.jsonl +11 -0
  80. package/memo/states-logs/trace-mljvvm1c-wbnqzp.jsonl +11 -0
  81. package/memo/states-logs/trace-mljxecwn-9xh3nw.jsonl +11 -0
  82. package/memo/states-logs/trace-mljxqkfm-ipijik.jsonl +11 -0
  83. package/memo/states-logs/trace-mljxwtrw-3fanky.jsonl +11 -0
  84. package/memo/states-logs/trace-mljxzen3-m8iinh.jsonl +11 -0
  85. package/memo/states-logs/trace-mljyucef-td6odn.jsonl +11 -0
  86. package/memo/states-logs/trace-mljyuprw-b1a6f4.jsonl +11 -0
  87. package/memo/states-logs/trace-mljyvefl-b6yoce.jsonl +11 -0
  88. package/memo/states-logs/trace-mljyxjo4-n7ibj2.jsonl +13 -0
  89. package/memo/states-logs/trace-mljziez5-8drqtn.jsonl +13 -0
  90. package/memo/states-logs/trace-mljziulp-dtd03z.jsonl +13 -0
  91. package/memo/states-logs/trace-mljzjwrq-1p2krb.jsonl +13 -0
  92. package/memo/states-logs/trace-mljzl0i7-b1cqa6.jsonl +13 -0
  93. package/memo/states-logs/trace-mljzmlk6-7kdyls.jsonl +13 -0
  94. package/memo/states-logs/trace-mlk0oj25-xa3dcu.jsonl +13 -0
  95. package/memo/states-logs/trace-mlk1x59q-713huj.jsonl +14 -0
  96. package/memo/states-logs/trace-mlk22dz8-7fd6hq.jsonl +14 -0
  97. package/memo/states-logs/trace-mlk241uy-wmx907.jsonl +14 -0
  98. package/memo/states-logs/trace-mlk2bf5r-yoh1vg.jsonl +15 -0
  99. package/package.json +44 -0
  100. package/pnpm-workspace.yaml +4 -0
  101. package/prompt_improvement_example.md +55 -0
  102. package/remove.txt +1 -0
  103. package/tests/adapters.test.ts +128 -0
  104. package/tests/extractCode.test.ts +26 -0
  105. package/tests/integration.test.ts +83 -0
  106. package/tests/loadConfig.test.ts +25 -0
  107. package/tests/navigatorBoundary.test.ts +17 -0
  108. package/tests/ports.test.ts +84 -0
  109. package/tests/runAllTests.ts +49 -0
  110. package/tests/toolCalls.test.ts +149 -0
  111. package/tests/waterfall.test.ts +52 -0
  112. package/tsconfig.json +32 -0
  113. package/tsup.config.ts +17 -0
  114. package/types.d.ts +96 -0
  115. package/util/ANSI.ts +29 -0
  116. package/util/extractCode.ts +217 -0
  117. package/util/extractToolCalls.ts +80 -0
  118. package/util/logger.ts +125 -0
@@ -0,0 +1,112 @@
1
+ # Credenciais Linux (credLinux)
2
+
3
+ Módulo para gerenciar credenciais usando o **secret-tool** do Linux (keyring nativo).
4
+
5
+ ## Requisitos
6
+
7
+ - Linux com `secret-tool` instalado (já vem na maioria das distros)
8
+ - Funciona em qualquer desktop environment (GNOME, KDE, XFCE, etc.)
9
+
10
+ ## Importação
11
+
12
+ ```typescript
13
+ import { saveCredLinux, getCredLinux, deleteCredLinux } from '@memo/desktop/pass';
14
+ ```
15
+
16
+ ## Funções
17
+
18
+ ### `saveCredLinux()`
19
+
20
+ Salva uma credencial no keyring do sistema.
21
+
22
+ ```typescript
23
+ const success = await saveCredLinux({
24
+ password: 'minha-senha-secreta',
25
+ label: 'GitHub Token',
26
+ account: 'gh-token',
27
+ service: 'meu-app' // opcional, default: 'box-safe'
28
+ });
29
+
30
+ // Retorna: true (sucesso) | false (erro)
31
+ ```
32
+
33
+ ### `getCredLinux()`
34
+
35
+ Recupera uma credencial salva.
36
+
37
+ ```typescript
38
+ const token = await getCredLinux({
39
+ account: 'gh-token',
40
+ service: 'meu-app' // opcional, default: 'box-safe'
41
+ });
42
+
43
+ // Retorna: string (senha) | null (não encontrada)
44
+ ```
45
+
46
+ ### `deleteCredLinux()`
47
+
48
+ Remove uma credencial do keyring.
49
+
50
+ ```typescript
51
+ const deleted = await deleteCredLinux({
52
+ account: 'gh-token',
53
+ service: 'meu-app' // opcional, default: 'box-safe'
54
+ });
55
+
56
+ // Retorna: true (sucesso) | false (erro)
57
+ ```
58
+
59
+ ## Exemplo Completo
60
+
61
+ ```typescript
62
+ // Salvar API key
63
+ await saveCredLinux({
64
+ password: 'sk-xxxxxxxxxxxxx',
65
+ label: 'OpenAI API Key',
66
+ account: 'openai-key'
67
+ });
68
+
69
+ // Usar depois
70
+ const apiKey = await getCredLinux({ account: 'openai-key' });
71
+ if (apiKey) {
72
+ console.log('Key encontrada:', apiKey);
73
+ }
74
+
75
+ // Limpar quando não precisar mais
76
+ await deleteCredLinux({ account: 'openai-key' });
77
+ ```
78
+
79
+ ## Parâmetros
80
+
81
+ ### CredentialArgs (save)
82
+ - `password`: senha/token a salvar
83
+ - `label`: descrição amigável (aparece na GUI do keyring)
84
+ - `account`: identificador único da credencial
85
+ - `service`: (opcional) nome do app/serviço (default: `box-safe`)
86
+
87
+ ### LookupArgs (get/delete)
88
+ - `account`: identificador da credencial
89
+ - `service`: (opcional) nome do app/serviço (default: `box-safe`)
90
+
91
+ ## Onde ficam salvos?
92
+
93
+ As credenciais são criptografadas e salvas em:
94
+ ```
95
+ ~/.local/share/keyrings/
96
+ ```
97
+
98
+ Você pode visualizar usando `seahorse` (GNOME) ou a GUI do seu keyring.
99
+
100
+ ## Logs
101
+
102
+ O módulo só loga erros no console. Se algo falhar:
103
+ - `saveCredLinux`: retorna `false`
104
+ - `getCredLinux`: retorna `null`
105
+ - `deleteCredLinux`: retorna `false`
106
+
107
+ Tratamento de erro fica por sua conta.
108
+
109
+
110
+ ## WINDOWS
111
+ for windows we use keytar
112
+
@@ -0,0 +1,82 @@
1
+ /***
2
+ * @fileoverview
3
+ * Manages credentials using Linux secret-tool
4
+ * @module
5
+ * memo/desktop/pass
6
+ ***/
7
+ import { exec } from 'child_process';
8
+ import { promisify } from 'util';
9
+ import { ANSI } from '@util/ANSI';
10
+ import { Logger } from '@core/util/logger';
11
+
12
+ const execAsync = promisify(exec);
13
+
14
+ interface CredentialArgs {
15
+ password: string;
16
+ label: string;
17
+ account: string;
18
+ service?: string;
19
+ }
20
+
21
+ interface LookupArgs {
22
+ account: string;
23
+ service?: string;
24
+ }
25
+
26
+ const DEFAULT_SERVICE = 'box-safe';
27
+ const logger = Logger.createModuleLogger('CredentialManager');
28
+
29
+ /**
30
+ * Saves credential to the system keyring
31
+ */
32
+ export async function setCredLinux({
33
+ password,
34
+ label,
35
+ account,
36
+ service = DEFAULT_SERVICE,
37
+ }: CredentialArgs): Promise<boolean> {
38
+ try {
39
+ await execAsync(
40
+ `echo -n "${password}" | secret-tool store --label="${label}" service ${service} account ${account}`
41
+ );
42
+ return true;
43
+ } catch (err) {
44
+ logger.error(`Failed to save credential: ${err}`);
45
+ return false;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Retrieves credential from the keyring
51
+ */
52
+ export async function getCredLinux({
53
+ account,
54
+ service = DEFAULT_SERVICE,
55
+ }: LookupArgs): Promise<string | null> {
56
+ try {
57
+ const { stdout } = await execAsync(
58
+ `secret-tool lookup service ${service} account ${account}`
59
+ );
60
+ return stdout.trim();
61
+ } catch (err) {
62
+ logger.error(`Credential not found: ${account}`);
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Deletes credential from the keyring
69
+ */
70
+ export async function deleteCredLinux({
71
+ account,
72
+ service = DEFAULT_SERVICE,
73
+ }: LookupArgs): Promise<boolean> {
74
+ try {
75
+ await execAsync(`secret-tool clear service ${service} account ${account}`);
76
+ return true;
77
+ } catch (err) {
78
+ logger.error(`Failed to delete credential: ${account}`);
79
+ return false;
80
+ }
81
+ }
82
+
@@ -0,0 +1,2 @@
1
+
2
+ // import keytar from 'keytar'; // use to set, get, delete credentials in windows
@@ -0,0 +1,67 @@
1
+ // Types will be imported from loadConfig to avoid external dependencies
2
+
3
+ export const DEFAULT_BOXSAFE_CONFIG = {
4
+ project: {
5
+ workspace: './',
6
+ testDir: './',
7
+ versionControl: {
8
+ before: false,
9
+ after: false,
10
+ generateNotes: false,
11
+ },
12
+ },
13
+ model: {
14
+ primary: {
15
+ provider: 'google',
16
+ name: 'gemini-2.5-flash',
17
+ },
18
+ fallback: [],
19
+ endpoint: null,
20
+ parameters: {},
21
+ },
22
+ smartRotation: {
23
+ enabled: false,
24
+ simple: [],
25
+ complex: [],
26
+ },
27
+ limits: {
28
+ tokens: 100000,
29
+ loops: 10,
30
+ timeout: {
31
+ enabled: false,
32
+ duration: '1h',
33
+ notify: true,
34
+ },
35
+ },
36
+ sandbox: {
37
+ enabled: true,
38
+ engine: 'docker',
39
+ memory: '512m',
40
+ cpu: 0.5,
41
+ network: 'none',
42
+ },
43
+ commands: {
44
+ setup: 'npm install',
45
+ run: 'echo OK',
46
+ test: null,
47
+ timeoutMs: 60_000,
48
+ },
49
+ interface: {
50
+ channel: 'terminal',
51
+ prompt: '',
52
+ notifications: {
53
+ whatsapp: false,
54
+ telegram: false,
55
+ slack: false,
56
+ email: false,
57
+ },
58
+ },
59
+ paths: {
60
+ generatedMarkdown: './memo/generated/codelog.md',
61
+ artifactOutput: './out.ts',
62
+ },
63
+ teach: {
64
+ urls: [],
65
+ files: [],
66
+ },
67
+ };
@@ -0,0 +1 @@
1
+ export { DEFAULT_BOXSAFE_CONFIG } from './boxsafeDefaults';
@@ -0,0 +1,133 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DEFAULT_BOXSAFE_CONFIG } from '@core/config/defaults';
4
+
5
+ // Local types to maintain core independence
6
+ export interface BoxSafeConfig {
7
+ project?: {
8
+ workspace?: string;
9
+ testDir?: string;
10
+ versionControl?: {
11
+ before?: boolean;
12
+ after?: boolean;
13
+ autoPush?: boolean;
14
+ generateNotes?: boolean;
15
+ };
16
+ };
17
+ model?: {
18
+ primary?: {
19
+ provider?: string;
20
+ name?: string;
21
+ };
22
+ fallback?: any[];
23
+ endpoint?: any;
24
+ parameters?: Record<string, any>;
25
+ };
26
+ smartRotation?: {
27
+ enabled?: boolean;
28
+ simple?: any[];
29
+ complex?: any[];
30
+ };
31
+ limits?: {
32
+ tokens?: number;
33
+ loops?: number;
34
+ timeout?: {
35
+ enabled?: boolean;
36
+ duration?: string;
37
+ notify?: boolean;
38
+ };
39
+ };
40
+ sandbox?: {
41
+ enabled?: boolean;
42
+ engine?: string;
43
+ memory?: string;
44
+ cpu?: number;
45
+ network?: string;
46
+ };
47
+ commands?: {
48
+ setup?: string;
49
+ run?: string;
50
+ test?: string | null;
51
+ timeoutMs?: number;
52
+ };
53
+ interface?: {
54
+ channel?: string;
55
+ prompt?: string;
56
+ notifications?: {
57
+ whatsapp?: boolean;
58
+ telegram?: boolean;
59
+ slack?: boolean;
60
+ email?: boolean;
61
+ };
62
+ };
63
+ paths?: {
64
+ generatedMarkdown?: string;
65
+ artifactOutput?: string;
66
+ };
67
+ teach?: {
68
+ urls?: string[];
69
+ files?: string[];
70
+ };
71
+ }
72
+
73
+ export type NormalizedBoxSafeConfig = Omit<BoxSafeConfig, 'limits'> & {
74
+ limits?: Omit<NonNullable<BoxSafeConfig['limits']>, 'loops'> & {
75
+ loops?: number;
76
+ };
77
+ };
78
+
79
+ type LoadConfigResult = {
80
+ config: NormalizedBoxSafeConfig;
81
+ source: { path: string; loaded: boolean };
82
+ };
83
+
84
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
85
+ return Boolean(v) && typeof v === 'object' && !Array.isArray(v);
86
+ }
87
+
88
+ function deepMerge<T>(base: T, override: unknown): T {
89
+ if (!isPlainObject(base) || !isPlainObject(override)) return (override ?? base) as T;
90
+
91
+ const out: Record<string, unknown> = { ...(base as any) };
92
+ for (const [k, v] of Object.entries(override)) {
93
+ const bv = (base as any)[k];
94
+ if (isPlainObject(bv) && isPlainObject(v)) out[k] = deepMerge(bv, v);
95
+ else out[k] = v;
96
+ }
97
+ return out as T;
98
+ }
99
+
100
+ function normalizeLoops(v: unknown, fallback: number): number {
101
+ if (typeof v === 'number' && Number.isFinite(v)) return v;
102
+ if (typeof v === 'string') {
103
+ const trimmed = v.trim().toLowerCase();
104
+ if (trimmed === 'infinity') return Number.MAX_SAFE_INTEGER;
105
+ const n = Number(trimmed);
106
+ if (Number.isFinite(n)) return n;
107
+ }
108
+ return fallback;
109
+ }
110
+
111
+ export function loadBoxSafeConfig(configPath?: string): LoadConfigResult {
112
+ const p = configPath ?? path.resolve(process.cwd(), 'boxsafe.config.json');
113
+
114
+ let rawConfig: unknown = null;
115
+ try {
116
+ if (fs.existsSync(p)) {
117
+ rawConfig = JSON.parse(fs.readFileSync(p, 'utf-8'));
118
+ }
119
+ } catch {
120
+ rawConfig = null;
121
+ }
122
+
123
+ const merged = deepMerge(DEFAULT_BOXSAFE_CONFIG, rawConfig ?? {});
124
+
125
+ const loopsFallback = typeof DEFAULT_BOXSAFE_CONFIG.limits?.loops === 'number' ? DEFAULT_BOXSAFE_CONFIG.limits.loops : 2;
126
+ if (!merged.limits) merged.limits = {} as any;
127
+ (merged.limits as any).loops = normalizeLoops((merged.limits as any).loops, loopsFallback);
128
+
129
+ return {
130
+ config: merged as NormalizedBoxSafeConfig,
131
+ source: { path: p, loaded: Boolean(rawConfig) },
132
+ };
133
+ }
@@ -0,0 +1,13 @@
1
+ Execução
2
+
3
+ [exit code]
4
+
5
+ [stderr crítico]
6
+
7
+ [contrato de saída]
8
+
9
+ [testes]
10
+
11
+ [verificações semânticas]
12
+
13
+ ✅ SUCESSO
@@ -0,0 +1,20 @@
1
+ import { loadBoxSafeConfig } from '@core/config/loadConfig';
2
+ import type { NormalizedBoxSafeConfig } from '@core/config/loadConfig';
3
+
4
+ export function loadBoxConfig(configPath?: string): NormalizedBoxSafeConfig {
5
+ return loadBoxSafeConfig(configPath).config;
6
+ }
7
+
8
+ export function getVersionControlFlags(boxConfig: NormalizedBoxSafeConfig): {
9
+ vcBefore: boolean;
10
+ vcAfter: boolean;
11
+ vcGenerateNotes: boolean;
12
+ vcAutoPushConfig: boolean;
13
+ } {
14
+ return {
15
+ vcBefore: Boolean(boxConfig.project?.versionControl?.before ?? false),
16
+ vcAfter: Boolean(boxConfig.project?.versionControl?.after ?? false),
17
+ vcGenerateNotes: Boolean(boxConfig.project?.versionControl?.generateNotes ?? false),
18
+ vcAutoPushConfig: Boolean(boxConfig.project?.versionControl?.autoPush ?? false),
19
+ };
20
+ }
@@ -0,0 +1,76 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Logger } from '@core/util/logger';
4
+ import { ANSI } from '@util/ANSI';
5
+
6
+ type AnsiLike = { Cyan: string; Yellow: string; Reset: string };
7
+
8
+ type BuildExecCommandArgs = {
9
+ cmd: any;
10
+ lang: string;
11
+ pathOutput: string;
12
+ };
13
+
14
+ const logger = Logger.createModuleLogger('BuildExecCommand');
15
+
16
+ export async function buildExecCommand({ cmd, lang, pathOutput }: BuildExecCommandArgs): Promise<any> {
17
+ // If caller left the default `echo OK`, automatically run the
18
+ // generated output file according to the requested language so the
19
+ // loop actually executes the produced artifact.
20
+ let execCmd = cmd;
21
+ if (typeof cmd === 'string' && cmd === 'echo OK') {
22
+ if (lang === 'ts') execCmd = ['tsx', [pathOutput]];
23
+ else if (lang === 'js') execCmd = ['node', [pathOutput]];
24
+ else if (lang === 'py' || lang === 'python') execCmd = ['python', [pathOutput]];
25
+ else if (lang === 'sh' || lang === 'bash' || lang === 'shell') execCmd = ['bash', [pathOutput]];
26
+ else execCmd = `${pathOutput}`;
27
+
28
+ const display = typeof execCmd === 'string' ? execCmd : Array.isArray(execCmd) ? `${execCmd[0]} ${execCmd[1].join(' ')}` : String(execCmd);
29
+ logger.info(`auto-executing generated file with: ${display}`);
30
+ }
31
+
32
+ // If executing JS: check for CommonJS usage (require) and project type=module.
33
+ try {
34
+ const isNodeExec =
35
+ lang === 'js' &&
36
+ ((typeof execCmd === 'string' && execCmd.trimStart().startsWith('node ')) ||
37
+ (Array.isArray(execCmd) && String(execCmd[0]) === 'node'));
38
+
39
+ if (isNodeExec) {
40
+ const pkgPath = path.join(process.cwd(), 'package.json');
41
+ let isModuleType = false;
42
+ try {
43
+ if (fs.existsSync(pkgPath)) {
44
+ const pkgRaw = fs.readFileSync(pkgPath, 'utf-8');
45
+ const pkg = JSON.parse(pkgRaw);
46
+ isModuleType = pkg.type === 'module';
47
+ }
48
+ } catch {
49
+ isModuleType = false;
50
+ }
51
+
52
+ const outContent = fs.readFileSync(pathOutput, 'utf-8');
53
+ if (isModuleType && /\brequire\s*\(/.test(outContent) && path.extname(pathOutput) === '.js') {
54
+ const newPath = pathOutput.replace(/\.js$/, '.cjs');
55
+ try {
56
+ await fs.promises.rename(pathOutput, newPath);
57
+ execCmd = ['node', [newPath]];
58
+ logger.info(`renamed output to ${newPath} for CommonJS compatibility`);
59
+ // create a minimal ESM wrapper at the original path so artifact exists
60
+ try {
61
+ const wrapper = `import { spawnSync } from 'node:child_process';\nimport path from 'node:path';\nconst target = path.join(path.dirname(new URL(import.meta.url).pathname), '${path.basename(newPath)}');\nconst res = spawnSync('node', [target], { stdio: 'inherit' });\nif (res.error) throw res.error;\nprocess.exit(res.status ?? 0);\n`;
62
+ await fs.promises.writeFile(pathOutput, wrapper, 'utf-8');
63
+ } catch {
64
+ // ignore
65
+ }
66
+ } catch {
67
+ logger.warn(`failed to rename file`);
68
+ }
69
+ }
70
+ }
71
+ } catch {
72
+ // non-fatal
73
+ }
74
+
75
+ return execCmd;
76
+ }
@@ -0,0 +1,121 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import type { CommandRun } from "../../../types";
4
+ import type { ExecResult } from "@core/loop/waterfall";
5
+ import { STATES_LOGS_DIR, STATES_LOG_FILE } from "@core/paths/paths";
6
+
7
+ type ExecodeOptions = {
8
+ timeoutMs?: number;
9
+ allowUnsafeShell?: boolean;
10
+ };
11
+
12
+ const DEFAULT_MAX_RUNTIME_MS = 60_000;
13
+
14
+ function getEffectiveTimeoutMs(timeoutMs?: number): number {
15
+ const fromEnv = process.env.BOXSAFE_CMD_TIMEOUT_MS ? Number(process.env.BOXSAFE_CMD_TIMEOUT_MS) : NaN;
16
+ if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
17
+ if (typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0) return timeoutMs;
18
+ return DEFAULT_MAX_RUNTIME_MS;
19
+ }
20
+
21
+ function containsShellOperators(s: string): boolean {
22
+ return /[;&|`]|\$\(|\$\{|>>?|<\(|<|\n|\r/.test(s);
23
+ }
24
+
25
+ function isObviouslyDangerousCommand(s: string): boolean {
26
+ const x = s.toLowerCase();
27
+ return (
28
+ /\brm\s+-rf\b/.test(x) ||
29
+ /\bmkfs\b/.test(x) ||
30
+ /\bdd\b\s+if=/.test(x) ||
31
+ /\bshutdown\b/.test(x) ||
32
+ /\breboot\b/.test(x)
33
+ );
34
+ }
35
+
36
+ function shouldAllowUnsafeShell(options?: ExecodeOptions): boolean {
37
+ const fromEnv = process.env.BOXSAFE_ALLOW_UNSAFE_SHELL?.toLowerCase();
38
+ if (fromEnv === 'true' || fromEnv === '1' || fromEnv === 'yes') return true;
39
+ return Boolean(options?.allowUnsafeShell);
40
+ }
41
+
42
+ export async function execode(
43
+ command: CommandRun,
44
+ options?: ExecodeOptions
45
+ ): Promise<ExecResult> {
46
+ const maxRuntimeMs = getEffectiveTimeoutMs(options?.timeoutMs);
47
+ const normalized = normalizeCommand(command);
48
+ const { cmd, args, useShell } = normalized;
49
+
50
+ if (useShell) {
51
+ const allowUnsafe = shouldAllowUnsafeShell(options);
52
+ if (!allowUnsafe) {
53
+ if (containsShellOperators(cmd) || isObviouslyDangerousCommand(cmd)) {
54
+ const stderr = `Blocked potentially unsafe shell command: ${cmd}`;
55
+ await mkdir(STATES_LOGS_DIR, { recursive: true });
56
+ await writeFile(STATES_LOG_FILE, stderr, "utf8");
57
+ return { exitCode: 126, stdout: "", stderr };
58
+ }
59
+ }
60
+ }
61
+
62
+ await mkdir(STATES_LOGS_DIR, { recursive: true });
63
+
64
+ return new Promise<ExecResult>((resolve, reject) => {
65
+ const child = spawn(cmd, args, { shell: useShell });
66
+
67
+ let stdout = "";
68
+ let stderr = "";
69
+ let timedOut = false;
70
+
71
+ child.stdout?.on("data", (data) => {
72
+ stdout += data.toString();
73
+ });
74
+
75
+ child.stderr?.on("data", (data) => {
76
+ stderr += data.toString();
77
+ });
78
+
79
+ const timer = setTimeout(() => {
80
+ timedOut = true;
81
+ child.kill("SIGTERM");
82
+ }, maxRuntimeMs);
83
+
84
+ child.on("error", (err) => {
85
+ clearTimeout(timer);
86
+ reject(err);
87
+ });
88
+
89
+ child.on("close", async (code, signal) => {
90
+ clearTimeout(timer);
91
+
92
+ const log = [
93
+ `exitCode=${code ?? 0}`,
94
+ `signal=${signal ?? "none"}`,
95
+ `timedOut=${timedOut}`,
96
+ `stdout:`, stdout,
97
+ `stderr:`, stderr,
98
+ ].join("\n");
99
+
100
+ await writeFile(STATES_LOG_FILE, log, "utf8");
101
+
102
+ resolve({
103
+ exitCode: code ?? 0,
104
+ stdout,
105
+ stderr,
106
+ });
107
+ });
108
+ });
109
+ }
110
+
111
+ function normalizeCommand(
112
+ command: CommandRun
113
+ ): { cmd: string; args: string[]; useShell: boolean } {
114
+ if (typeof command === "string") {
115
+ return { cmd: command, args: [], useShell: true };
116
+ }
117
+
118
+ const [cmd, args] = command;
119
+ return { cmd, args, useShell: false };
120
+ }
121
+
@@ -0,0 +1,3 @@
1
+ const { Logger } = require('@coreutil/logger');
2
+ const logger = Logger.createModuleLogger('TestJS');
3
+ logger.info("hello world");