agentic-workflow-manager 2.0.0 → 2.1.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.
@@ -9,6 +9,7 @@ exports.registerDoctorCommand = registerDoctorCommand;
9
9
  const picocolors_1 = __importDefault(require("picocolors"));
10
10
  const context_1 = require("../core/diagnostics/context");
11
11
  const checks_1 = require("../core/diagnostics/checks");
12
+ const paths_1 = require("../core/paths");
12
13
  function glyph(status) {
13
14
  if (status === 'ok')
14
15
  return picocolors_1.default.green('✔');
@@ -30,24 +31,25 @@ function line(r) {
30
31
  }
31
32
  function renderReport(report) {
32
33
  const lines = [];
33
- lines.push(picocolors_1.default.bold('AWM · estado del harness'));
34
+ lines.push(picocolors_1.default.bold('AWM · harness status'));
34
35
  lines.push('');
35
- lines.push('Máquina (global)');
36
+ lines.push('Machine (global)');
37
+ lines.push(picocolors_1.default.dim(` platform: ${(0, paths_1.platformLabel)()}`));
36
38
  for (const r of report.results.filter((x) => x.level === 'machine'))
37
39
  lines.push(line(r));
38
40
  lines.push('');
39
41
  if (report.hasProject) {
40
- lines.push(`Proyecto: ${report.projectName ?? ''}`.trimEnd());
42
+ lines.push(`Project: ${report.projectName ?? ''}`.trimEnd());
41
43
  for (const r of report.results.filter((x) => x.level === 'project'))
42
44
  lines.push(line(r));
43
45
  }
44
46
  else {
45
- lines.push(picocolors_1.default.dim('(sin proyecto en el cwd)'));
47
+ lines.push(picocolors_1.default.dim('(no project in cwd)'));
46
48
  }
47
49
  lines.push('');
48
50
  const actions = report.results.filter((r) => r.remedy.kind !== 'none').length;
49
- const estado = report.overall === 'healthy' ? picocolors_1.default.green('sano') : picocolors_1.default.red('degradado');
50
- lines.push(`estado: ${estado} · ${actions} acciones sugeridas`);
51
+ const status = report.overall === 'healthy' ? picocolors_1.default.green('healthy') : picocolors_1.default.red('degraded');
52
+ lines.push(`status: ${status} · ${actions} suggested actions`);
51
53
  return lines.join('\n');
52
54
  }
53
55
  function runDoctor(opts = {}) {
@@ -56,7 +58,7 @@ function runDoctor(opts = {}) {
56
58
  report = (0, checks_1.runChecks)((0, context_1.gatherContext)({ cwd: opts.cwd }));
57
59
  }
58
60
  catch (err) {
59
- process.stderr.write(`awm doctor: error interno: ${err.message}\n`);
61
+ process.stderr.write(`awm doctor: internal error: ${err.message}\n`);
60
62
  return 2;
61
63
  }
62
64
  if (opts.json) {
@@ -8,6 +8,7 @@ exports.installHook = installHook;
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const providers_1 = require("../../providers");
11
+ const paths_1 = require("../../core/paths");
11
12
  function syncFile(source, dest, method) {
12
13
  try {
13
14
  fs_1.default.unlinkSync(dest);
@@ -26,8 +27,7 @@ function syncFile(source, dest, method) {
26
27
  function backupSettings(settingsPath) {
27
28
  if (!fs_1.default.existsSync(settingsPath))
28
29
  return null;
29
- const awmHome = process.env.AWM_HOME || path_1.default.join(process.env.HOME, '.awm');
30
- const backupDir = path_1.default.join(awmHome, 'backups');
30
+ const backupDir = path_1.default.join((0, paths_1.awmHome)(), 'backups');
31
31
  fs_1.default.mkdirSync(backupDir, { recursive: true });
32
32
  const ts = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '-').slice(0, 19);
33
33
  const backupPath = path_1.default.join(backupDir, `settings.json.${ts}.bak`);
@@ -57,13 +57,19 @@ function installHook(options) {
57
57
  fs_1.default.mkdirSync(config.scriptsDir, { recursive: true });
58
58
  syncFile(path_1.default.join(sourceHooks, 'session-start'), path_1.default.join(config.scriptsDir, 'session-start'), options.installMethod);
59
59
  syncFile(path_1.default.join(sourceHooks, 'run-hook.cmd'), path_1.default.join(config.scriptsDir, 'run-hook.cmd'), options.installMethod);
60
- // 3. Symlink the skill (ALWAYS symlink so awm update propagates)
60
+ // 3. Link the skill (default: symlink so 'awm update' propagates; fall back to copy if symlink is unavailable, e.g. Windows without Developer Mode)
61
61
  const skillDest = path_1.default.join(config.scriptsDir, 'using-awm.md');
62
62
  try {
63
63
  fs_1.default.unlinkSync(skillDest);
64
64
  }
65
65
  catch { /* not exists */ }
66
- fs_1.default.symlinkSync(sourceSkill, skillDest);
66
+ try {
67
+ fs_1.default.symlinkSync(sourceSkill, skillDest);
68
+ }
69
+ catch {
70
+ // best-effort: copy the single skill file; 'awm update' will not auto-propagate
71
+ fs_1.default.copyFileSync(sourceSkill, skillDest);
72
+ }
67
73
  // 4. Backup settings if it exists
68
74
  const backupPath = backupSettings(config.settingsPath);
69
75
  // 5. Read or initialize settings
@@ -7,11 +7,11 @@ exports.uninstallHook = uninstallHook;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const providers_1 = require("../../providers");
10
+ const paths_1 = require("../../core/paths");
10
11
  function backupSettings(settingsPath) {
11
12
  if (!fs_1.default.existsSync(settingsPath))
12
13
  return null;
13
- const awmHome = process.env.AWM_HOME || path_1.default.join(process.env.HOME, '.awm');
14
- const backupDir = path_1.default.join(awmHome, 'backups');
14
+ const backupDir = path_1.default.join((0, paths_1.awmHome)(), 'backups');
15
15
  fs_1.default.mkdirSync(backupDir, { recursive: true });
16
16
  const ts = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '-').slice(0, 19);
17
17
  const backupPath = path_1.default.join(backupDir, `settings.json.${ts}.bak`);
@@ -49,6 +49,7 @@ const bundles_1 = require("../core/bundles");
49
49
  const registries_1 = require("../core/registries");
50
50
  const orchestrator_1 = require("../core/init/orchestrator");
51
51
  const steps_1 = require("../core/init/steps");
52
+ const paths_1 = require("../core/paths");
52
53
  // ---------------------------------------------------------------------------
53
54
  // Rendering
54
55
  // ---------------------------------------------------------------------------
@@ -65,26 +66,26 @@ function renderInitOutcome(o) {
65
66
  const lines = [];
66
67
  lines.push(picocolors_1.default.bold('AWM · init'));
67
68
  lines.push('');
68
- // --- Estado inicial ---
69
- lines.push(picocolors_1.default.bold('Estado inicial'));
69
+ // --- Initial state ---
70
+ lines.push(picocolors_1.default.bold('Initial state'));
70
71
  lines.push((0, doctor_1.renderReport)(o.before));
71
72
  lines.push('');
72
- // --- Acciones ---
73
- lines.push(picocolors_1.default.bold('Acciones'));
73
+ // --- Actions ---
74
+ lines.push(picocolors_1.default.bold('Actions'));
74
75
  for (const s of o.steps) {
75
76
  const det = s.detail ? picocolors_1.default.dim(` ${s.detail}`) : '';
76
77
  const err = s.error ? picocolors_1.default.red(` [${s.error}]`) : '';
77
78
  lines.push(` ${stepGlyph(s.action)} ${s.id}${det}${err}`);
78
79
  }
79
80
  lines.push('');
80
- // --- Estado final ---
81
- lines.push(picocolors_1.default.bold('Estado final'));
81
+ // --- Final state ---
82
+ lines.push(picocolors_1.default.bold('Final state'));
82
83
  lines.push((0, doctor_1.renderReport)(o.after));
83
84
  lines.push('');
84
85
  // --- Summary ---
85
86
  const pendingCount = o.pending;
86
- const estado = o.after.overall === 'healthy' ? picocolors_1.default.green('sano') : picocolors_1.default.red('degradado');
87
- lines.push(`estado: ${estado} · ${pendingCount} pasos requieren un agente (skills arriba)`);
87
+ const status = o.after.overall === 'healthy' ? picocolors_1.default.green('healthy') : picocolors_1.default.red('degraded');
88
+ lines.push(`status: ${status} · ${pendingCount} steps require an agent (skills above)`);
88
89
  return lines.join('\n');
89
90
  }
90
91
  async function runInit(opts = {}) {
@@ -121,7 +122,7 @@ async function runInit(opts = {}) {
121
122
  });
122
123
  }
123
124
  catch (err) {
124
- process.stderr.write(`awm init: error interno: ${err.message}\n`);
125
+ process.stderr.write(`awm init: internal error: ${err.message}\n`);
125
126
  return 2;
126
127
  }
127
128
  if (opts.json) {
@@ -149,7 +150,7 @@ function makeConfirmExtensions(yes) {
149
150
  return [];
150
151
  const { multiselect, isCancel } = await Promise.resolve().then(() => __importStar(require('@clack/prompts')));
151
152
  const choice = await multiselect({
152
- message: `Extensiones detectadas (${signals.join(', ')}) — ¿activar?`,
153
+ message: `Extensions detected (${signals.join(', ')}) — activate?`,
153
154
  options: proposed.map((p) => ({ value: p, label: p })),
154
155
  initialValues: proposed,
155
156
  required: false,
@@ -170,6 +171,7 @@ function registerInitCommand(program) {
170
171
  .option('--machine-only', 'Only run machine-level steps (skip project steps)')
171
172
  .option('--json', 'Emit the InitOutcome as JSON')
172
173
  .action(async (options) => {
174
+ (0, paths_1.warnIfUnsupportedPlatform)((m) => console.warn(picocolors_1.default.yellow(`⚠ ${m}`)));
173
175
  const code = await runInit({
174
176
  yes: options.yes,
175
177
  agent: options.agent,
@@ -124,8 +124,8 @@ function registerRegistryCommand(program) {
124
124
  console.log(`${picocolors_1.default.cyan(r.name)} ${r.remote} ${picocolors_1.default.dim(counts)}`);
125
125
  for (const o of (0, status_1.overrideStatus)(r.contentRoot, earlier)) {
126
126
  console.log(o.active
127
- ? picocolors_1.default.yellow(` ↑ override activo: ${o.name}`)
128
- : picocolors_1.default.dim(` ∅ override sin efecto: ${o.name}`));
127
+ ? picocolors_1.default.yellow(` ↑ override active: ${o.name}`)
128
+ : picocolors_1.default.dim(` ∅ override with no effect: ${o.name}`));
129
129
  }
130
130
  }
131
131
  catch (e) {
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.parseEslintOutput = parseEslintOutput;
7
+ const path_1 = __importDefault(require("path"));
4
8
  function parseEslintOutput(raw) {
5
9
  let parsed;
6
10
  try {
@@ -15,7 +19,9 @@ function parseEslintOutput(raw) {
15
19
  for (const msg of file.messages) {
16
20
  if (msg.severity < 2 || msg.line == null || msg.column == null)
17
21
  continue;
18
- const rel = file.filePath.startsWith(cwd + '/') ? file.filePath.slice(cwd.length + 1) : file.filePath;
22
+ const rel = file.filePath.startsWith(cwd + path_1.default.sep)
23
+ ? path_1.default.relative(cwd, file.filePath)
24
+ : file.filePath;
19
25
  errors.push({
20
26
  file: rel,
21
27
  line: msg.line,
@@ -58,8 +58,8 @@ function registerSensorsCommand(program) {
58
58
  const writeDir = manifestDir ?? process.cwd();
59
59
  (0, baseline_1.writeBaseline)(writeDir, baseline);
60
60
  const total = Object.values(baseline).reduce((n, fps) => n + fps.length, 0);
61
- prompts_1.log.success(`Baseline guardado: ${total} hallazgos aceptados en .awm/sensors.baseline.json`);
62
- prompts_1.log.info('Los sensors ahora fallan solo ante hallazgos nuevos. Re-corré `awm sensors baseline` tras reducir deuda.');
61
+ prompts_1.log.success(`Baseline saved: ${total} findings accepted in .awm/sensors.baseline.json`);
62
+ prompts_1.log.info('Sensors now fail only on new findings. Re-run `awm sensors baseline` after reducing debt.');
63
63
  });
64
64
  sensors
65
65
  .command('status')
@@ -7,12 +7,12 @@ exports.installSensorHook = installSensorHook;
7
7
  exports.uninstallSensorHook = uninstallSensorHook;
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
- const os_1 = __importDefault(require("os"));
10
+ const paths_1 = require("../../core/paths");
11
11
  const POST_TOOL_USE_EVENT = 'PostToolUse';
12
12
  const POST_TOOL_USE_MATCHER = 'Write|Edit|MultiEdit';
13
13
  const AWM_SENSOR_CMD = 'awm sensors run --fast';
14
14
  function defaultSettingsPath() {
15
- return path_1.default.join(process.env.HOME ?? os_1.default.homedir(), '.claude', 'settings.json');
15
+ return path_1.default.join((0, paths_1.homeDir)(), '.claude', 'settings.json');
16
16
  }
17
17
  function readSettings(p) {
18
18
  if (!fs_1.default.existsSync(p))
@@ -31,8 +31,7 @@ function isAwmEntry(e) {
31
31
  function backupSettings(settingsPath) {
32
32
  if (!fs_1.default.existsSync(settingsPath))
33
33
  return undefined;
34
- const awmHome = process.env.AWM_HOME || path_1.default.join(process.env.HOME ?? os_1.default.homedir(), '.awm');
35
- const backupDir = path_1.default.join(awmHome, 'backups');
34
+ const backupDir = path_1.default.join((0, paths_1.awmHome)(), 'backups');
36
35
  fs_1.default.mkdirSync(backupDir, { recursive: true });
37
36
  const ts = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '-').slice(0, 19);
38
37
  const backupPath = path_1.default.join(backupDir, `settings.json.${ts}.sensor.bak`);
@@ -20,7 +20,7 @@ function configCheck(parts, cwd) {
20
20
  const i = parts.indexOf('--config');
21
21
  const cfg = i !== -1 ? parts[i + 1] : undefined;
22
22
  if (cfg && !fs_1.default.existsSync(path_1.default.join(cwd, cfg))) {
23
- return { ok: false, detail: `config faltante: ${cfg}` };
23
+ return { ok: false, detail: `missing config: ${cfg}` };
24
24
  }
25
25
  return null;
26
26
  }
@@ -38,12 +38,12 @@ function checkCmd(cmd, cwd) {
38
38
  if (bin === 'npx') {
39
39
  const tool = npxTool(parts);
40
40
  if (!tool)
41
- return { ok: false, detail: 'npx sin tool especificada' };
41
+ return { ok: false, detail: 'npx without a tool specified' };
42
42
  const localBin = path_1.default.join(cwd, 'node_modules', '.bin', tool);
43
43
  if (!fs_1.default.existsSync(localBin)) {
44
44
  return {
45
45
  ok: false,
46
- detail: `${tool} no instalada localmente (npx bajaría un paquete remoto) — agregala a devDependencies`,
46
+ detail: `${tool} not installed locally (npx would download a remote package) — add it to devDependencies`,
47
47
  };
48
48
  }
49
49
  return configCheck(parts, cwd) ?? { ok: true, detail: `${tool} (node_modules/.bin)` };
@@ -7,14 +7,11 @@ exports.globalContextPath = globalContextPath;
7
7
  exports.materialize = materialize;
8
8
  // cli/src/core/context/materializer.ts
9
9
  const fs_1 = __importDefault(require("fs"));
10
- const os_1 = __importDefault(require("os"));
11
10
  const path_1 = __importDefault(require("path"));
12
11
  const provider_1 = require("./provider");
13
- function awmHome() {
14
- return process.env.AWM_HOME || path_1.default.join(process.env.HOME || os_1.default.homedir(), '.awm');
15
- }
12
+ const paths_1 = require("../paths");
16
13
  function globalContextPath() {
17
- return path_1.default.join(awmHome(), 'context', 'awm-context.md');
14
+ return path_1.default.join((0, paths_1.awmHome)(), 'context', 'awm-context.md');
18
15
  }
19
16
  function materialize(ctx, absPath, scope) {
20
17
  let onDisk = null;
@@ -22,8 +22,8 @@ function buildContext(input) {
22
22
  throw new Error(`using-awm skill not found at ${skillPath}. Run 'awm update' first.`);
23
23
  }
24
24
  const skill = fs_1.default.readFileSync(skillPath, 'utf-8');
25
- const exts = input.profileExtensions.length ? input.profileExtensions.join(', ') : 'ninguna';
26
- const header = `<!-- AWM context (generated) -->\n# AWM\n\nExtensiones activas: ${exts}\n\n`;
25
+ const exts = input.profileExtensions.length ? input.profileExtensions.join(', ') : 'none';
26
+ const header = `<!-- AWM context (generated) -->\n# AWM\n\nActive extensions: ${exts}\n\n`;
27
27
  const markdown = header + skill;
28
28
  return { markdown, sourceVersion: parseVersion(skill), contentHash: sha256(markdown) };
29
29
  }
@@ -21,7 +21,7 @@ function machineChecks(m) {
21
21
  }
22
22
  else if (m.registryCache.gitState === 'behind') {
23
23
  out.push({ id: 'machine.cli', level: 'machine', label: 'CLI', status: 'warn',
24
- detail: 'cache desactualizado', remedy: cmd('awm update') });
24
+ detail: 'cache out of date', remedy: cmd('awm update') });
25
25
  }
26
26
  else {
27
27
  // dirty | unknown | undefined → advisory, sin acción
@@ -34,7 +34,7 @@ function machineChecks(m) {
34
34
  }
35
35
  else if (m.hook.present) {
36
36
  out.push({ id: 'machine.hook', level: 'machine', label: 'hook SessionStart', status: 'warn',
37
- detail: 'scripts incompletos', remedy: cmd('awm init') });
37
+ detail: 'incomplete scripts', remedy: cmd('awm init') });
38
38
  }
39
39
  else {
40
40
  out.push({ id: 'machine.hook', level: 'machine', label: 'hook SessionStart', status: 'missing',
@@ -46,7 +46,7 @@ function machineChecks(m) {
46
46
  }
47
47
  else if (m.devCore.present) {
48
48
  out.push({ id: 'machine.devCore', level: 'machine', label: 'dev-core (baseline)', status: 'warn',
49
- detail: `${m.devCore.brokenLinks.length} symlinks rotos`, remedy: cmd('awm init') });
49
+ detail: `${m.devCore.brokenLinks.length} broken symlinks`, remedy: cmd('awm init') });
50
50
  }
51
51
  else {
52
52
  out.push({ id: 'machine.devCore', level: 'machine', label: 'dev-core (baseline)', status: 'missing',
@@ -55,11 +55,11 @@ function machineChecks(m) {
55
55
  // machine.globalSkills — integridad de symlinks en ~/.claude/skills (fuera del baseline)
56
56
  const brokenGlobal = m.globalSkills.repairable.length + m.globalSkills.dead.length;
57
57
  if (brokenGlobal === 0) {
58
- out.push({ id: 'machine.globalSkills', level: 'machine', label: 'skills globales', status: 'ok', remedy: none });
58
+ out.push({ id: 'machine.globalSkills', level: 'machine', label: 'global skills', status: 'ok', remedy: none });
59
59
  }
60
60
  else {
61
- out.push({ id: 'machine.globalSkills', level: 'machine', label: 'skills globales', status: 'warn',
62
- detail: `${brokenGlobal} enlaces rotos`, remedy: cmd('awm init') });
61
+ out.push({ id: 'machine.globalSkills', level: 'machine', label: 'global skills', status: 'warn',
62
+ detail: `${brokenGlobal} broken links`, remedy: cmd('awm init') });
63
63
  }
64
64
  // machine.ambient.<b> — una fila por bundle deseado
65
65
  for (const b of m.ambient.wanted) {
@@ -70,15 +70,15 @@ function machineChecks(m) {
70
70
  // machine.context.<agent> — una fila por agente con contexto AWM gestionado
71
71
  for (const c of m.contextInjection) {
72
72
  if (c.state === 'injected') {
73
- out.push({ id: `machine.context.${c.agent}`, level: 'machine', label: `contexto AWM (${c.agent})`,
73
+ out.push({ id: `machine.context.${c.agent}`, level: 'machine', label: `AWM context (${c.agent})`,
74
74
  status: 'ok', remedy: none });
75
75
  }
76
76
  else if (c.state === 'stale') {
77
- out.push({ id: `machine.context.${c.agent}`, level: 'machine', label: `contexto AWM (${c.agent})`,
78
- status: 'warn', detail: 'contexto desactualizado', remedy: cmd('awm init') });
77
+ out.push({ id: `machine.context.${c.agent}`, level: 'machine', label: `AWM context (${c.agent})`,
78
+ status: 'warn', detail: 'context out of date', remedy: cmd('awm init') });
79
79
  }
80
80
  else {
81
- out.push({ id: `machine.context.${c.agent}`, level: 'machine', label: `contexto AWM (${c.agent})`,
81
+ out.push({ id: `machine.context.${c.agent}`, level: 'machine', label: `AWM context (${c.agent})`,
82
82
  status: 'missing', remedy: cmd('awm init') });
83
83
  }
84
84
  }
@@ -88,7 +88,7 @@ function projectChecks(p) {
88
88
  const out = [];
89
89
  // project.profile
90
90
  if (p.profile.present) {
91
- const exts = p.profile.extensions.length ? p.profile.extensions.join(', ') : 'sin extensiones';
91
+ const exts = p.profile.extensions.length ? p.profile.extensions.join(', ') : 'no extensions';
92
92
  out.push({ id: 'project.profile', level: 'project', label: `.awm/profile.json (${exts})`,
93
93
  status: 'ok', remedy: none });
94
94
  }
@@ -99,21 +99,21 @@ function projectChecks(p) {
99
99
  // project.activation
100
100
  const missingLinks = p.activeBundles.expected.filter((s) => !p.activeBundles.linked.includes(s));
101
101
  if (p.activeBundles.broken.length === 0 && missingLinks.length === 0) {
102
- out.push({ id: 'project.activation', level: 'project', label: 'bundles activos', status: 'ok', remedy: none });
102
+ out.push({ id: 'project.activation', level: 'project', label: 'active bundles', status: 'ok', remedy: none });
103
103
  }
104
104
  else {
105
- out.push({ id: 'project.activation', level: 'project', label: 'bundles activos', status: 'missing',
106
- detail: `${missingLinks.length} faltan, ${p.activeBundles.broken.length} rotos`, remedy: cmd('awm sync') });
105
+ out.push({ id: 'project.activation', level: 'project', label: 'active bundles', status: 'missing',
106
+ detail: `${missingLinks.length} missing, ${p.activeBundles.broken.length} broken`, remedy: cmd('awm sync') });
107
107
  }
108
108
  // project.sensors
109
109
  out.push(p.sensors.present
110
- ? { id: 'project.sensors', level: 'project', label: 'sensores', status: 'ok', remedy: none }
111
- : { id: 'project.sensors', level: 'project', label: 'sensores no inicializados', status: 'missing',
110
+ ? { id: 'project.sensors', level: 'project', label: 'sensors', status: 'ok', remedy: none }
111
+ : { id: 'project.sensors', level: 'project', label: 'sensors not initialized', status: 'missing',
112
112
  remedy: cmd('awm sensors init') });
113
113
  // project.constitution (missing degrada; remedio agente)
114
114
  out.push(p.constitution.present
115
115
  ? { id: 'project.constitution', level: 'project', label: 'CONSTITUTION.md', status: 'ok', remedy: none }
116
- : { id: 'project.constitution', level: 'project', label: 'CONSTITUTION.md ausente', status: 'missing',
116
+ : { id: 'project.constitution', level: 'project', label: 'CONSTITUTION.md missing', status: 'missing',
117
117
  remedy: skillRemedy('project-constitution') });
118
118
  // project.context (advisory; no degrada)
119
119
  if (p.context.present) {
@@ -121,7 +121,7 @@ function projectChecks(p) {
121
121
  status: 'ok', remedy: none });
122
122
  }
123
123
  else {
124
- out.push({ id: 'project.context', level: 'project', label: 'contexto del agente (CLAUDE.md/AGENTS.md) ausente', status: 'warn',
124
+ out.push({ id: 'project.context', level: 'project', label: 'agent context (CLAUDE.md/AGENTS.md) missing', status: 'warn',
125
125
  remedy: skillRemedy('project-context-init') });
126
126
  }
127
127
  return out;
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.gatherContext = gatherContext;
7
7
  // src/core/diagnostics/context.ts
8
8
  const fs_1 = __importDefault(require("fs"));
9
- const os_1 = __importDefault(require("os"));
10
9
  const path_1 = __importDefault(require("path"));
11
10
  const child_process_1 = require("child_process");
12
11
  const providers_1 = require("../../providers");
@@ -16,8 +15,7 @@ const status_1 = require("../../commands/hooks/status");
16
15
  const profile_1 = require("../profile");
17
16
  const bundles_1 = require("../bundles");
18
17
  const skill_integrity_1 = require("../skill-integrity");
19
- function home() { return process.env.HOME || os_1.default.homedir(); }
20
- function awmHome() { return process.env.AWM_HOME || path_1.default.join(home(), '.awm'); }
18
+ const paths_1 = require("../paths");
21
19
  // Estado de un artefacto en <dir>/<skill>: link vivo / symlink colgante / ausente.
22
20
  function linkState(dir, skill) {
23
21
  const p = path_1.default.join(dir, skill);
@@ -122,7 +120,7 @@ function gatherMachine(bundles, agent = 'claude-code') {
122
120
  // ambient (deseados desde ~/.awm/config.json)
123
121
  let wanted = [];
124
122
  try {
125
- const cfg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(awmHome(), 'config.json'), 'utf-8'));
123
+ const cfg = JSON.parse(fs_1.default.readFileSync(path_1.default.join((0, paths_1.awmHome)(), 'config.json'), 'utf-8'));
126
124
  if (Array.isArray(cfg.ambient)) {
127
125
  wanted = cfg.ambient.filter((x) => typeof x === 'string');
128
126
  }
@@ -170,10 +170,10 @@ async function stepProfile(d) {
170
170
  const alreadyPresent = proj.profile.extensions;
171
171
  const newProposed = proposed.filter((p) => !alreadyPresent.includes(p));
172
172
  if (newProposed.length === 0)
173
- return bootstrapOrSkip(d, proj, 'sin extensiones nuevas');
173
+ return bootstrapOrSkip(d, proj, 'no new extensions');
174
174
  const confirmed = await d.confirmExtensions(newProposed, signals);
175
175
  if (confirmed.length === 0)
176
- return bootstrapOrSkip(d, proj, 'sin extensiones confirmadas');
176
+ return bootstrapOrSkip(d, proj, 'no extensions confirmed');
177
177
  for (const name of confirmed) {
178
178
  d.actions.addExtension(proj.root, name);
179
179
  }
@@ -188,7 +188,7 @@ async function stepProfile(d) {
188
188
  function bootstrapOrSkip(d, proj, skipDetail) {
189
189
  if (!proj.profile.present) {
190
190
  d.actions.ensureProfile(proj.root);
191
- return ok('project.profile', 'project', 'applied', 'perfil inicializado (sin extensiones)');
191
+ return ok('project.profile', 'project', 'applied', 'profile initialized (no extensions)');
192
192
  }
193
193
  return ok('project.profile', 'project', 'skipped', skipDetail);
194
194
  }
@@ -239,10 +239,10 @@ function stepConstitutionInjection(d) {
239
239
  return ok('project.constitutionInjection', 'project', 'skipped', 'no project');
240
240
  const inj = (0, providers_1.getInjection)(d.agent);
241
241
  if (!inj || inj.type !== 'config-instructions') {
242
- return ok('project.constitutionInjection', 'project', 'skipped', 'cubierto por hook');
242
+ return ok('project.constitutionInjection', 'project', 'skipped', 'covered by hook');
243
243
  }
244
244
  if (!proj.constitution.present) {
245
- return ok('project.constitutionInjection', 'project', 'skipped', 'sin CONSTITUTION.md');
245
+ return ok('project.constitutionInjection', 'project', 'skipped', 'no CONSTITUTION.md');
246
246
  }
247
247
  const res = d.actions.injectProjectConstitution({ projectRoot: proj.root, agent: d.agent });
248
248
  if (res === 'injected')
@@ -262,9 +262,9 @@ function stepContext(d) {
262
262
  function stepContextInjection(d) {
263
263
  const inj = (0, providers_1.getInjection)(d.agent);
264
264
  if (!inj)
265
- return ok('machine.contextInjection', 'machine', 'skipped', 'sin mecanismo de inyección');
265
+ return ok('machine.contextInjection', 'machine', 'skipped', 'no injection mechanism');
266
266
  if (inj.type === 'cc-settings-merge')
267
- return ok('machine.contextInjection', 'machine', 'skipped', 'cubierto por hook');
267
+ return ok('machine.contextInjection', 'machine', 'skipped', 'covered by hook');
268
268
  const op = {
269
269
  agent: d.agent,
270
270
  scope: 'global',
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WINDOWS_NATIVE_WARNING = void 0;
7
+ exports.homeDir = homeDir;
8
+ exports.awmHome = awmHome;
9
+ exports.platform = platform;
10
+ exports.isWindowsNative = isWindowsNative;
11
+ exports.platformLabel = platformLabel;
12
+ exports.warnIfUnsupportedPlatform = warnIfUnsupportedPlatform;
13
+ // cli/src/core/paths.ts
14
+ //
15
+ // Single source of truth for home / AWM_HOME resolution and platform detection.
16
+ // Functions are evaluated at CALL TIME (not require time) so env overrides are
17
+ // always honored and tests need no jest.resetModules().
18
+ const os_1 = __importDefault(require("os"));
19
+ const path_1 = __importDefault(require("path"));
20
+ /** User home directory with a robust fallback. Never returns a raw, possibly-empty process.env.HOME. */
21
+ function homeDir() {
22
+ return process.env.HOME || os_1.default.homedir();
23
+ }
24
+ /** AWM home directory (~/.awm), honoring the AWM_HOME override. */
25
+ function awmHome() {
26
+ return process.env.AWM_HOME || path_1.default.join(homeDir(), '.awm');
27
+ }
28
+ /** Raw platform string (wrapper over process.platform for testability). */
29
+ function platform() {
30
+ return process.platform;
31
+ }
32
+ /** True only on native Windows. WSL reports 'linux', so this returns false there. */
33
+ function isWindowsNative() {
34
+ return platform() === 'win32';
35
+ }
36
+ /** Human-friendly platform label for diagnostics. */
37
+ function platformLabel() {
38
+ switch (platform()) {
39
+ case 'win32':
40
+ return 'Windows (native — not supported yet, use WSL)';
41
+ case 'darwin':
42
+ return 'macOS';
43
+ case 'linux':
44
+ return 'Linux';
45
+ default:
46
+ return platform();
47
+ }
48
+ }
49
+ exports.WINDOWS_NATIVE_WARNING = 'AWM detected native Windows. Native support is deferred; the recommended path today is WSL.\n' +
50
+ ' Install WSL (https://learn.microsoft.com/windows/wsl/install) and run AWM inside your Linux distro.\n' +
51
+ ' Continuing in best-effort mode, but some steps (symlinks, hooks) may not work.';
52
+ /** Emit the unsupported-platform warning via the provided logger, only on native Windows. */
53
+ function warnIfUnsupportedPlatform(log) {
54
+ if (isWindowsNative())
55
+ log(exports.WINDOWS_NATIVE_WARNING);
56
+ }
@@ -19,13 +19,13 @@ exports.verifyMinCliVersions = verifyMinCliVersions;
19
19
  // src/core/registries.ts
20
20
  const fs_1 = __importDefault(require("fs"));
21
21
  const path_1 = __importDefault(require("path"));
22
- const os_1 = __importDefault(require("os"));
23
22
  const simple_git_1 = __importDefault(require("simple-git"));
24
23
  const registry_1 = require("./registry");
25
24
  const versioning_1 = require("./versioning");
26
25
  const cli_version_1 = require("./cli-version");
27
- // Evaluated at require-time — tests must use jest.resetModules() + late require() to pick up env overrides.
28
- const AWM_HOME = process.env.AWM_HOME || path_1.default.join(process.env.HOME || os_1.default.homedir(), '.awm');
26
+ const paths_1 = require("./paths");
27
+ // Computed at require-time by calling the shared resolver (single source of truth: core/paths.ts).
28
+ const AWM_HOME = (0, paths_1.awmHome)();
29
29
  exports.REGISTRIES_DIR = path_1.default.join(AWM_HOME, 'registries');
30
30
  exports.REGISTRIES_CONFIG_PATH = path_1.default.join(AWM_HOME, 'registries.json');
31
31
  exports.CONTENT_DIR_NAMES = ['skills', 'bundles', 'workflows', 'agents'];
@@ -16,17 +16,16 @@ exports.offerSelfUpdate = offerSelfUpdate;
16
16
  // `awm update`. AWM_NO_UPDATE_CHECK=1 desactiva ambas (tests, CI).
17
17
  const fs_1 = __importDefault(require("fs"));
18
18
  const path_1 = __importDefault(require("path"));
19
- const os_1 = __importDefault(require("os"));
20
19
  const child_process_1 = require("child_process");
21
20
  const picocolors_1 = __importDefault(require("picocolors"));
22
21
  const prompts_1 = require("@clack/prompts");
23
22
  const cli_version_1 = require("./cli-version");
24
23
  const versioning_1 = require("./versioning");
24
+ const paths_1 = require("./paths");
25
25
  const TTL_MS = 24 * 60 * 60 * 1000;
26
26
  const REGISTRY_URL = `https://registry.npmjs.org/${cli_version_1.CLI_PACKAGE_NAME}/latest`;
27
27
  function cacheFile() {
28
- const home = process.env.AWM_HOME || path_1.default.join(process.env.HOME || os_1.default.homedir(), '.awm');
29
- return path_1.default.join(home, 'update-check.json');
28
+ return path_1.default.join((0, paths_1.awmHome)(), 'update-check.json');
30
29
  }
31
30
  function readUpdateCache() {
32
31
  try {
@@ -72,7 +71,7 @@ function maybeNotifyUpdate(opts) {
72
71
  const spawnWorker = opts?.spawnWorker ?? spawnRefreshWorker;
73
72
  const cache = readUpdateCache();
74
73
  if (cache?.latest && (0, versioning_1.compareSemver)(cache.latest, (0, cli_version_1.cliVersion)()) > 0) {
75
- console.log(picocolors_1.default.dim(`\n⬆ awm v${cache.latest} disponible → npm i -g ${cli_version_1.CLI_PACKAGE_NAME}`));
74
+ console.log(picocolors_1.default.dim(`\n⬆ awm v${cache.latest} available → npm i -g ${cli_version_1.CLI_PACKAGE_NAME}`));
76
75
  }
77
76
  if (!cache || now - cache.lastCheck > TTL_MS)
78
77
  spawnWorker();
@@ -90,17 +89,17 @@ async function offerSelfUpdate(deps = {}) {
90
89
  const r = await (0, prompts_1.confirm)({ message });
91
90
  return !(0, prompts_1.isCancel)(r) && r === true;
92
91
  });
93
- const yes = await confirmImpl(`¿Actualizar awm v${current} → v${latest} ahora?`);
92
+ const yes = await confirmImpl(`Update awm v${current} → v${latest} now?`);
94
93
  if (!yes) {
95
- console.log(picocolors_1.default.dim(` Para actualizar después: npm i -g ${cli_version_1.CLI_PACKAGE_NAME}`));
94
+ console.log(picocolors_1.default.dim(` To update later: npm i -g ${cli_version_1.CLI_PACKAGE_NAME}`));
96
95
  return;
97
96
  }
98
97
  const runner = deps.runner ?? ((cmd, args) => (0, child_process_1.spawnSync)(cmd, args, { stdio: 'inherit', shell: true }));
99
98
  const r = runner('npm', ['i', '-g', `${cli_version_1.CLI_PACKAGE_NAME}@latest`]);
100
99
  if (r.status === 0) {
101
- console.log(picocolors_1.default.green(` ✓ CLI actualizado a v${latest} (aplica desde el próximo comando)`));
100
+ console.log(picocolors_1.default.green(` ✓ CLI updated to v${latest} (takes effect from the next command)`));
102
101
  }
103
102
  else {
104
- console.warn(picocolors_1.default.yellow(` ⚠ No se pudo actualizar automáticamente corré: npm i -g ${cli_version_1.CLI_PACKAGE_NAME}`));
103
+ console.warn(picocolors_1.default.yellow(` ⚠ Automatic update failedrun: npm i -g ${cli_version_1.CLI_PACKAGE_NAME}`));
105
104
  }
106
105
  }
package/dist/src/index.js CHANGED
@@ -34,6 +34,7 @@ const registry_1 = require("./commands/registry");
34
34
  const pin_1 = require("./commands/pin");
35
35
  const profile_pins_1 = require("./core/profile-pins");
36
36
  const update_check_1 = require("./core/update-check");
37
+ const paths_1 = require("./core/paths");
37
38
  const program = new commander_1.Command();
38
39
  program.name('awm').description('Agentic Workflow Manager').version((0, cli_version_1.cliVersion)());
39
40
  program.hook('postAction', () => {
@@ -321,7 +322,7 @@ program.command('update')
321
322
  }
322
323
  }
323
324
  for (const f of (0, registries_1.verifyMinCliVersions)()) {
324
- console.warn(picocolors_1.default.yellow(` ⚠ El registry ${f.name} requiere CLI ≥ ${f.min} (tenés ${(0, cli_version_1.cliVersion)()}) — corré: npm i -g agentic-workflow-manager`));
325
+ console.warn(picocolors_1.default.yellow(` ⚠ Registry ${f.name} requires CLI ≥ ${f.min} (you have ${(0, cli_version_1.cliVersion)()}) — run: npm i -g agentic-workflow-manager`));
325
326
  }
326
327
  try {
327
328
  const regen = (0, regenerate_1.regenerateGlobalContext)();
@@ -351,7 +352,7 @@ program.command('update')
351
352
  }
352
353
  catch { /* no aborta */ }
353
354
  await (0, update_check_1.offerSelfUpdate)(); // capa 2 — Task 13
354
- (0, prompts_1.outro)('✅ Registries, skills y hooks actualizados.');
355
+ (0, prompts_1.outro)('✅ Registries, skills and hooks updated.');
355
356
  });
356
357
  program.command('sync')
357
358
  .description('Rebuild local skill symlinks from .awm/profile.json (e.g. after cloning on a new machine)')
@@ -359,6 +360,7 @@ program.command('sync')
359
360
  .option('-m, --method <method>', 'Install method: symlink or copy', 'symlink')
360
361
  .action(async (options) => {
361
362
  (0, prompts_1.intro)(picocolors_1.default.bgCyan(picocolors_1.default.black(' AWM - Sync Project Profile ')));
363
+ (0, paths_1.warnIfUnsupportedPlatform)((m) => console.warn(picocolors_1.default.yellow(`⚠ ${m}`)));
362
364
  const projectRoot = (0, profile_1.findProjectRoot)(process.cwd());
363
365
  if (!projectRoot) {
364
366
  console.error(picocolors_1.default.red('No project root found (need a .git/, package.json, or .awm/profile.json here).'));
@@ -385,8 +387,8 @@ program.command('sync')
385
387
  const cliFailures = (0, registries_1.verifyMinCliVersions)();
386
388
  if (cliFailures.length > 0) {
387
389
  for (const f of cliFailures) {
388
- console.error(picocolors_1.default.red(`El registry ${f.name} requiere CLI ≥ ${f.min} (tenés ${(0, cli_version_1.cliVersion)()}).`));
389
- console.error(picocolors_1.default.red(' Corré: npm i -g agentic-workflow-manager'));
390
+ console.error(picocolors_1.default.red(`Registry ${f.name} requires CLI ≥ ${f.min} (you have ${(0, cli_version_1.cliVersion)()}).`));
391
+ console.error(picocolors_1.default.red(' Run: npm i -g agentic-workflow-manager'));
390
392
  }
391
393
  process.exit(1);
392
394
  }
@@ -408,8 +410,8 @@ program.command('sync')
408
410
  }
409
411
  }
410
412
  else {
411
- console.error(picocolors_1.default.red(`La máquina tiene ${f.name} @ ${f.actual ? `v${f.actual}` : 'HEAD (sin tag)'} pero el proyecto requiere v${f.required}.`));
412
- console.error(picocolors_1.default.red(` Corré: awm pin ${f.name} ${f.required} && awm update`));
413
+ console.error(picocolors_1.default.red(`This machine has ${f.name} @ ${f.actual ? `v${f.actual}` : 'HEAD (no tag)'} but the project requires v${f.required}.`));
414
+ console.error(picocolors_1.default.red(` Run: awm pin ${f.name} ${f.required} && awm update`));
413
415
  }
414
416
  }
415
417
  process.exit(1);
@@ -8,10 +8,10 @@ exports.getTargetPath = getTargetPath;
8
8
  exports.getHookConfig = getHookConfig;
9
9
  exports.getInjection = getInjection;
10
10
  // src/providers/index.ts
11
- const os_1 = __importDefault(require("os"));
12
11
  const path_1 = __importDefault(require("path"));
13
- const homedir = process.env.HOME || os_1.default.homedir();
14
- const awmHome = process.env.AWM_HOME || path_1.default.join(homedir, '.awm');
12
+ const paths_1 = require("../core/paths");
13
+ const homedir = (0, paths_1.homeDir)();
14
+ const awmHomeDir = (0, paths_1.awmHome)();
15
15
  exports.PROVIDERS = {
16
16
  antigravity: {
17
17
  label: 'Antigravity',
@@ -38,7 +38,7 @@ exports.PROVIDERS = {
38
38
  hooks: {
39
39
  type: 'cc-settings-merge',
40
40
  settingsPath: path_1.default.join(homedir, '.claude/settings.json'),
41
- scriptsDir: path_1.default.join(awmHome, 'hooks'),
41
+ scriptsDir: path_1.default.join(awmHomeDir, 'hooks'),
42
42
  matcher: 'startup|clear|compact',
43
43
  eventName: 'SessionStart'
44
44
  },
@@ -8,14 +8,14 @@ exports.savePreferences = savePreferences;
8
8
  // src/utils/config.ts
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
- const os_1 = __importDefault(require("os"));
11
+ const paths_1 = require("../core/paths");
12
12
  const DEFAULT_PREFS = {
13
13
  defaultAgent: 'antigravity',
14
14
  installMethod: 'symlink',
15
15
  defaultScope: 'local'
16
16
  };
17
17
  function prefsDir() {
18
- return process.env.AWM_HOME || path_1.default.join(process.env.HOME || os_1.default.homedir(), '.awm');
18
+ return (0, paths_1.awmHome)();
19
19
  }
20
20
  function getPreferences() {
21
21
  const file = path_1.default.join(prefsDir(), 'preferences.json');
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const doctor_1 = require("../../src/commands/doctor");
4
+ describe('doctor renderReport — platform line', () => {
5
+ const realPlatform = process.platform;
6
+ afterEach(() => {
7
+ Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
8
+ });
9
+ function emptyReport() {
10
+ return { overall: 'healthy', hasProject: false, projectName: undefined, results: [] };
11
+ }
12
+ it('renders the platform label under the Machine header', () => {
13
+ Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
14
+ const out = (0, doctor_1.renderReport)(emptyReport());
15
+ expect(out).toContain('platform: Linux');
16
+ });
17
+ it('flags native Windows with a WSL hint', () => {
18
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
19
+ const out = (0, doctor_1.renderReport)(emptyReport());
20
+ expect(out).toContain('WSL');
21
+ });
22
+ });
@@ -22,27 +22,27 @@ function report(partial = {}) {
22
22
  describe('renderReport', () => {
23
23
  it('renders the machine block with glyphs and remedies', () => {
24
24
  const out = (0, doctor_1.renderReport)(report());
25
- expect(out).toContain('AWM · estado del harness');
26
- expect(out).toContain('Máquina (global)');
25
+ expect(out).toContain('AWM · harness status');
26
+ expect(out).toContain('Machine (global)');
27
27
  expect(out).toContain('✔ CLI v1.0.0');
28
28
  expect(out).toContain('✖ hook SessionStart');
29
29
  expect(out).toContain('→ awm init');
30
- expect(out).toContain('estado: degradado · 1 acciones sugeridas');
30
+ expect(out).toContain('status: degraded · 1 suggested actions');
31
31
  });
32
32
  it('omits the project block and shows a hint when hasProject is false', () => {
33
33
  const out = (0, doctor_1.renderReport)(report());
34
- expect(out).toContain('(sin proyecto en el cwd)');
34
+ expect(out).toContain('(no project in cwd)');
35
35
  expect(out).not.toContain('Proyecto:');
36
36
  });
37
37
  it('renders the detail field in parentheses when present', () => {
38
38
  const out = (0, doctor_1.renderReport)(report({
39
39
  results: [
40
40
  { id: 'machine.cli', level: 'machine', label: 'CLI v1.2.0', status: 'warn',
41
- detail: 'cache desactualizado', remedy: { kind: 'command', value: 'awm update' } },
41
+ detail: 'cache out of date', remedy: { kind: 'command', value: 'awm update' } },
42
42
  ],
43
43
  }));
44
44
  expect(out).toContain('CLI v1.2.0');
45
- expect(out).toContain('(cache desactualizado)');
45
+ expect(out).toContain('(cache out of date)');
46
46
  expect(out).toContain('→ awm update');
47
47
  });
48
48
  it('renders the project block titled with projectName', () => {
@@ -50,11 +50,11 @@ describe('renderReport', () => {
50
50
  hasProject: true,
51
51
  projectName: 'belanz',
52
52
  results: [
53
- { id: 'project.constitution', level: 'project', label: 'CONSTITUTION.md ausente',
53
+ { id: 'project.constitution', level: 'project', label: 'CONSTITUTION.md missing',
54
54
  status: 'missing', remedy: { kind: 'skill', value: 'project-constitution' } },
55
55
  ],
56
56
  }));
57
- expect(out).toContain('Proyecto: belanz');
57
+ expect(out).toContain('Project: belanz');
58
58
  expect(out).toContain('→ skill: project-constitution');
59
59
  });
60
60
  });
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const os_1 = __importDefault(require("os"));
9
+ describe('hooks/install — skill symlink fallback to copy', () => {
10
+ let tmpHome;
11
+ let origHome;
12
+ let origAwmHome;
13
+ let symlinkSpy;
14
+ beforeEach(() => {
15
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-symlink-fb-'));
16
+ origHome = process.env.HOME;
17
+ origAwmHome = process.env.AWM_HOME;
18
+ process.env.HOME = tmpHome;
19
+ process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
20
+ jest.resetModules();
21
+ });
22
+ afterEach(() => {
23
+ symlinkSpy?.mockRestore();
24
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
25
+ if (origHome === undefined)
26
+ delete process.env.HOME;
27
+ else
28
+ process.env.HOME = origHome;
29
+ if (origAwmHome === undefined)
30
+ delete process.env.AWM_HOME;
31
+ else
32
+ process.env.AWM_HOME = origAwmHome;
33
+ });
34
+ function seedRegistry(root) {
35
+ const hooksDir = path_1.default.join(root, 'hooks');
36
+ const skillDir = path_1.default.join(root, 'skills', 'using-awm');
37
+ fs_1.default.mkdirSync(hooksDir, { recursive: true });
38
+ fs_1.default.mkdirSync(skillDir, { recursive: true });
39
+ fs_1.default.writeFileSync(path_1.default.join(hooksDir, 'session-start'), '#!/bin/sh\n');
40
+ fs_1.default.writeFileSync(path_1.default.join(hooksDir, 'run-hook.cmd'), '#!/bin/sh\n');
41
+ fs_1.default.writeFileSync(path_1.default.join(skillDir, 'SKILL.md'), '# using-awm\n');
42
+ }
43
+ it('copies the skill when symlink throws (EPERM), preserving content', () => {
44
+ const registryRoot = path_1.default.join(tmpHome, 'registry');
45
+ seedRegistry(registryRoot);
46
+ // Force symlinkSync to fail like a platform without symlink permission.
47
+ symlinkSpy = jest.spyOn(fs_1.default, 'symlinkSync').mockImplementation(() => {
48
+ const err = new Error('EPERM: operation not permitted, symlink');
49
+ err.code = 'EPERM';
50
+ throw err;
51
+ });
52
+ const { installHook } = require('../../../src/commands/hooks/install');
53
+ const result = installHook({ agent: 'claude-code', registryRoot, installMethod: 'copy' });
54
+ const skillDest = path_1.default.join(result.scriptsDir, 'using-awm.md');
55
+ expect(fs_1.default.existsSync(skillDest)).toBe(true);
56
+ expect(fs_1.default.lstatSync(skillDest).isSymbolicLink()).toBe(false); // it was copied, not linked
57
+ expect(fs_1.default.readFileSync(skillDest, 'utf-8')).toContain('using-awm');
58
+ });
59
+ });
@@ -35,13 +35,13 @@ describe('renderInitOutcome', () => {
35
35
  };
36
36
  const out = (0, init_1.renderInitOutcome)(outcome);
37
37
  expect(out).toContain('AWM · init');
38
- expect(out).toContain('Estado inicial');
39
- expect(out).toContain('Acciones');
38
+ expect(out).toContain('Initial state');
39
+ expect(out).toContain('Actions');
40
40
  expect(out).toContain('machine.cache');
41
41
  expect(out).toContain('skill: project-constitution');
42
- expect(out).toContain('Estado final');
43
- expect(out).toContain('AWM · estado del harness'); // viene de renderReport
44
- expect(out).toContain('1 pasos requieren un agente');
42
+ expect(out).toContain('Final state');
43
+ expect(out).toContain('AWM · harness status'); // viene de renderReport
44
+ expect(out).toContain('1 steps require an agent');
45
45
  });
46
46
  });
47
47
  describe('runInit', () => {
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const path_1 = __importDefault(require("path"));
7
+ const eslint_1 = require("../../../src/commands/sensors/formatters/eslint");
8
+ describe('parseEslintOutput — relative path normalization', () => {
9
+ it('produces a cwd-relative path using the OS separator', () => {
10
+ const cwd = process.cwd();
11
+ const abs = path_1.default.join(cwd, 'src', 'foo.ts');
12
+ const raw = JSON.stringify([
13
+ { filePath: abs, messages: [{ ruleId: 'no-eval', severity: 2, message: 'no eval', line: 3, column: 1 }] },
14
+ ]);
15
+ const errors = (0, eslint_1.parseEslintOutput)(raw);
16
+ expect(errors).toHaveLength(1);
17
+ const first = errors[0];
18
+ if (!first)
19
+ throw new Error('Expected at least one error');
20
+ // path.join('src','foo.ts') uses the OS separator; on POSIX this is 'src/foo.ts'
21
+ expect(first.file).toBe(path_1.default.join('src', 'foo.ts'));
22
+ const file = first.file ?? '';
23
+ expect(file.startsWith(path_1.default.sep)).toBe(false); // genuinely relative
24
+ });
25
+ it('preserves the absolute path for files outside cwd', () => {
26
+ const abs = '/some/other/project/file.ts';
27
+ const raw = JSON.stringify([
28
+ { filePath: abs, messages: [{ ruleId: 'no-eval', severity: 2, message: 'no eval', line: 1, column: 1 }] },
29
+ ]);
30
+ const errors = (0, eslint_1.parseEslintOutput)(raw);
31
+ expect(errors).toHaveLength(1);
32
+ expect(errors[0]?.file).toBe(abs); // absolute path preserved, no traversal strings
33
+ });
34
+ });
@@ -50,7 +50,7 @@ describe('computeSensorStatus', () => {
50
50
  const result = (0, status_1.computeSensorStatus)(tmpDir);
51
51
  expect(result.overall).toBe('DEGRADED');
52
52
  expect(result.checks.depcheck.ok).toBe(false);
53
- expect(result.checks.depcheck.detail).toMatch(/no instalada/i);
53
+ expect(result.checks.depcheck.detail).toMatch(/not installed locally/i);
54
54
  });
55
55
  it('marks a sensor DEGRADED when its --config file is missing', () => {
56
56
  installLocalBin('eslint');
@@ -61,7 +61,7 @@ describe('computeSensorStatus', () => {
61
61
  }));
62
62
  const result = (0, status_1.computeSensorStatus)(tmpDir);
63
63
  expect(result.checks.lint.ok).toBe(false);
64
- expect(result.checks.lint.detail).toMatch(/config faltante/i);
64
+ expect(result.checks.lint.detail).toMatch(/missing config/i);
65
65
  });
66
66
  it('is HEALTHY when the npx tool is installed and the --config file exists', () => {
67
67
  installLocalBin('eslint');
@@ -138,7 +138,7 @@ describe('runChecks — project', () => {
138
138
  const report = (0, checks_1.runChecks)({ machine: healthyMachine(), project: p });
139
139
  const c = report.results.find((r) => r.id === 'project.context');
140
140
  expect(c.status).toBe('warn');
141
- expect(c.label).toBe('contexto del agente (CLAUDE.md/AGENTS.md) ausente');
141
+ expect(c.label).toBe('agent context (CLAUDE.md/AGENTS.md) missing');
142
142
  expect(c.remedy).toEqual({ kind: 'skill', value: 'project-context-init' });
143
143
  expect(report.overall).toBe('healthy');
144
144
  });
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const os_1 = __importDefault(require("os"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const paths_1 = require("../../src/core/paths");
9
+ describe('core/paths', () => {
10
+ let origHome;
11
+ let origAwmHome;
12
+ const realPlatform = process.platform;
13
+ beforeEach(() => {
14
+ origHome = process.env.HOME;
15
+ origAwmHome = process.env.AWM_HOME;
16
+ });
17
+ afterEach(() => {
18
+ if (origHome === undefined)
19
+ delete process.env.HOME;
20
+ else
21
+ process.env.HOME = origHome;
22
+ if (origAwmHome === undefined)
23
+ delete process.env.AWM_HOME;
24
+ else
25
+ process.env.AWM_HOME = origAwmHome;
26
+ Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
27
+ });
28
+ function setPlatform(p) {
29
+ Object.defineProperty(process, 'platform', { value: p, configurable: true });
30
+ }
31
+ it('homeDir uses process.env.HOME when set', () => {
32
+ process.env.HOME = '/tmp/fake-home';
33
+ expect((0, paths_1.homeDir)()).toBe('/tmp/fake-home');
34
+ });
35
+ it('homeDir falls back to os.homedir() when HOME is unset', () => {
36
+ delete process.env.HOME;
37
+ expect((0, paths_1.homeDir)()).toBe(os_1.default.homedir());
38
+ });
39
+ it('awmHome honors AWM_HOME override', () => {
40
+ process.env.AWM_HOME = '/tmp/custom-awm';
41
+ expect((0, paths_1.awmHome)()).toBe('/tmp/custom-awm');
42
+ });
43
+ it('awmHome defaults to <home>/.awm when AWM_HOME is unset', () => {
44
+ delete process.env.AWM_HOME;
45
+ process.env.HOME = '/tmp/fake-home';
46
+ expect((0, paths_1.awmHome)()).toBe(path_1.default.join('/tmp/fake-home', '.awm'));
47
+ });
48
+ it('platform reflects process.platform', () => {
49
+ setPlatform('linux');
50
+ expect((0, paths_1.platform)()).toBe('linux');
51
+ });
52
+ it('isWindowsNative is true only on win32', () => {
53
+ setPlatform('win32');
54
+ expect((0, paths_1.isWindowsNative)()).toBe(true);
55
+ setPlatform('linux');
56
+ expect((0, paths_1.isWindowsNative)()).toBe(false);
57
+ setPlatform('darwin');
58
+ expect((0, paths_1.isWindowsNative)()).toBe(false);
59
+ });
60
+ it('platformLabel describes each known platform', () => {
61
+ setPlatform('linux');
62
+ expect((0, paths_1.platformLabel)()).toBe('Linux');
63
+ setPlatform('darwin');
64
+ expect((0, paths_1.platformLabel)()).toBe('macOS');
65
+ setPlatform('win32');
66
+ expect((0, paths_1.platformLabel)()).toContain('WSL');
67
+ });
68
+ it('warnIfUnsupportedPlatform calls the logger only on win32', () => {
69
+ const calls = [];
70
+ const log = (m) => calls.push(m);
71
+ setPlatform('linux');
72
+ (0, paths_1.warnIfUnsupportedPlatform)(log);
73
+ expect(calls).toHaveLength(0);
74
+ setPlatform('win32');
75
+ (0, paths_1.warnIfUnsupportedPlatform)(log);
76
+ expect(calls).toEqual([paths_1.WINDOWS_NATIVE_WARNING]);
77
+ });
78
+ });
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
7
7
  },
8
- "files": ["dist"],
8
+ "files": [
9
+ "dist"
10
+ ],
9
11
  "repository": {
10
12
  "type": "git",
11
13
  "url": "git+https://github.com/Kodria/agentic-workflow.git",