@aetheros/cli 0.0.0-stage → 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hariprasath Thanarajah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/bin/aether.js ADDED
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, writeFileSync, realpathSync } from 'node:fs';
3
+ import { resolve, basename } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { checkProject, installKit, KITS, gitHooksStatus, installGitHooks, uninstallGitHooks, GROUPS, addIdea, addOwnerItem, completeOwnerItem, decideIdea, ownerSummary, planIdea, readOwner } from '@aetheros/workflow';
6
+ import { installClaudeCodeTemplates } from '@aetheros/provider-claude';
7
+ import { installOpenAITemplates } from '@aetheros/provider-openai';
8
+ import { approveSpec, checkSpecRecords, evaluateApprovedSpecGate, linkSpec, listSpecs, specStatus } from '@aetheros/spec-kit';
9
+ const flag = (args, name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
10
+ const describe = s => `${s.spec}: ${s.approved ? 'approved by ' + s.approvedBy : s.stale ? 'changed since approval (' + s.changed.join(', ') + ')' : s.ready ? 'ready, not approved' : 'missing ' + s.missing.join(', ')}${s.feature ? ' | ' + s.feature : ''}${s.issue ? ' | #' + s.issue : ''}`;
11
+ import { checkMachine, setup } from '../src/setup.js';
12
+ import { runSprint } from '../src/sprint.js';
13
+ import { setupMachine } from '../src/machine.js';
14
+ import { createInterface } from 'node:readline/promises';
15
+ import { installMcpTemplates } from '@aetheros/provider-claude';
16
+ // Run only when started directly. Compare real paths: npm's bin folder and folders such as macOS /var are symlinks,
17
+ // while import.meta.url is always the real file.
18
+ const isMain = (() => { try { return !!process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href; } catch { return false; } })();
19
+ const INSTALLERS = { ...Object.fromEntries(Object.keys(KITS).map(k => [k, (root, o) => installKit(k, root, o)])), githooks: (root, o) => installGitHooks(root, o), claude: installClaudeCodeTemplates, openai: installOpenAITemplates, mcp: installMcpTemplates };
20
+ const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
21
+ const CONFIG = 'aether.config.json';
22
+ export function run(args, cwd = process.cwd(), log = console.log) {
23
+ const [command] = args;
24
+ const file = resolve(cwd, CONFIG);
25
+ if (command === 'init') {
26
+ if (existsSync(file)) throw new Error('Configuration already exists; refusing to overwrite');
27
+ const config = {schemaVersion:1,project:{name:basename(cwd)},providers:['claude','openai'],approval:'writes'};
28
+ writeFileSync(file, JSON.stringify(config,null,2)+'\n', {flag:'wx'});
29
+ log('Created '+file);
30
+ return;
31
+ }
32
+ if (command === 'version' || command === '--version') { log(VERSION); return; }
33
+ if (command === 'doctor' && args[1] === 'machine') {
34
+ const { ok, results } = checkMachine();
35
+ for (const r of results) log(`${r.ok ? 'ok ' : r.required ? 'MISSING ' : 'optional'} ${r.name}${r.version ? ' ' + r.version : ''}${r.hint && !r.ok ? ' — ' + r.hint : ''}`);
36
+ if (!ok) throw new Error('Required tools are missing');
37
+ log('This computer is ready.');
38
+ return;
39
+ }
40
+ if (command === 'doctor' || command === 'status') {
41
+ if (!existsSync(file)) throw new Error('Missing aether.config.json; run aether init');
42
+ const config = JSON.parse(readFileSync(file,'utf8'));
43
+ if (config.schemaVersion !== 1 || !config.project || typeof config.project.name !== 'string' || !Array.isArray(config.providers) || !['always','writes','never'].includes(config.approval) || config.providers.some(p => !['claude','openai'].includes(p))) throw new Error('Invalid AetherOS configuration');
44
+ log(command === 'doctor' ? 'Configuration valid: '+config.project.name : JSON.stringify({project:config.project.name,providers:config.providers,approval:config.approval},null,2));
45
+ return;
46
+ }
47
+ if (command === 'install') {
48
+ const kits = args.slice(1).filter(a => !a.startsWith('--'));
49
+ const dryRun = args.includes('--dry-run');
50
+ if (!kits.length || kits.some(k => !INSTALLERS[k])) throw new Error('Usage: aether install <' + Object.keys(INSTALLERS).join('|') + '>... [--dry-run]');
51
+ for (const kit of kits) {
52
+ const { created, skipped } = INSTALLERS[kit](cwd, { dryRun });
53
+ for (const f of created) log((dryRun ? 'would create ' : 'created ') + f);
54
+ for (const f of skipped) log('kept existing ' + f);
55
+ }
56
+ return;
57
+ }
58
+ if (command === 'githooks') {
59
+ const sub = args[1] || 'status';
60
+ if (sub === 'install') { const r = installGitHooks(cwd, { force: args.includes('--force'), dryRun: args.includes('--dry-run') }); for (const f of r.created) log((r.dryRun ? 'would create ' : 'created ') + f); log((r.dryRun ? 'would set' : 'set') + ' core.hooksPath to ' + r.hooksPath); return; }
61
+ if (sub === 'uninstall') { log(uninstallGitHooks(cwd).removed ? 'AetherOS git hooks turned off (files kept)' : 'AetherOS git hooks were not active'); return; }
62
+ if (sub === 'status') { const s = gitHooksStatus(cwd); log(s.installed ? 'AetherOS git hooks are active' : 'AetherOS git hooks are not active' + (s.hooksPath ? ` (core.hooksPath is ${s.hooksPath})` : '')); return; }
63
+ throw new Error('Usage: aether githooks <install [--force] [--dry-run]|uninstall|status>');
64
+ }
65
+ if (command === 'owner') {
66
+ const [, sub, ...rest] = args;
67
+ const words = rest.filter((a, i) => !a.startsWith('--') && !(i > 0 && rest[i - 1].startsWith('--')));
68
+ if (sub === 'add') { const item = addOwnerItem(cwd, { group: words[0], task: words.slice(1).join(' '), detail: flag(args, '--detail'), link: flag(args, '--link'), source: flag(args, '--source') }); log(`added ${item.id}: [${item.group}] ${item.task}`); return; }
69
+ if (sub === 'done') { const item = completeOwnerItem(cwd, words[0], { note: flag(args, '--note') }); log(`done ${item.id}: ${item.task}`); return; }
70
+ if (sub === 'list' || !sub) {
71
+ if (args.includes('--all')) { for (const i of readOwner(cwd).items) log(`${i.id} ${i.status.padEnd(4)} [${i.group}] ${i.task}`); return; }
72
+ const s = ownerSummary(cwd);
73
+ if (!s.openCount) log('Nothing is waiting on the owner.');
74
+ for (const g of s.open) { log(g.group + ':'); for (const i of g.items) log(` ${i.id} ${i.task}${i.link ? ' — ' + i.link : ''}`); }
75
+ if (s.toDecide.length) log(`Ideas to decide: ${s.toDecide.length} (aether idea list)`);
76
+ return;
77
+ }
78
+ throw new Error('Usage: aether owner <list [--all]|add <' + GROUPS.join('|') + '> <task> [--detail ..] [--link ..]|done <id> [--note ..]>');
79
+ }
80
+ if (command === 'idea') {
81
+ const [, sub, ...rest] = args;
82
+ const words = rest.filter((a, i) => !a.startsWith('--') && !(i > 0 && rest[i - 1].startsWith('--')));
83
+ if (sub === 'add') { const idea = addIdea(cwd, { title: words.join(' '), why: flag(args, '--why') }); log(`added idea ${idea.id}: ${idea.title} (waiting for the owner's decision)`); return; }
84
+ if (sub === 'approve' || sub === 'reject') { const idea = decideIdea(cwd, words[0], sub === 'approve' ? 'Approved' : 'Rejected', { note: flag(args, '--note') }); log(`${idea.status.toLowerCase()}: ${idea.title}`); return; }
85
+ if (sub === 'plan') { const idea = planIdea(cwd, words[0], { issue: Number(flag(args, '--issue')) }); log(`planned: ${idea.title} → #${idea.issue}`); return; }
86
+ if (sub === 'list' || !sub) { const { ideas } = readOwner(cwd); if (!ideas.length) log('No ideas yet.'); for (const i of ideas) log(`${i.id} ${i.status.padEnd(9)} ${i.title}${i.issue ? ' #' + i.issue : ''}`); return; }
87
+ throw new Error('Usage: aether idea <list|add <title> [--why ..]|approve <id>|reject <id>|plan <id> --issue <n>>');
88
+ }
89
+ if (command === 'spec') {
90
+ const [, sub, path] = args;
91
+ if (sub === 'list') { const all = listSpecs(cwd); if (!all.length) log('no specs/ directories'); for (const s of all) log(describe(s)); return; }
92
+ if (!path || !['status', 'link', 'approve', 'gate'].includes(sub)) throw new Error('Usage: aether spec <list|status <path>|link <path> [--feature <dir>] [--issue <n>]|approve <path> --by <name>|gate <path>>');
93
+ if (sub === 'status') { log(describe(specStatus(cwd, path))); return; }
94
+ if (sub === 'link') { const issue = flag(args, '--issue'); linkSpec(cwd, path, { feature: flag(args, '--feature'), issue: issue === undefined ? undefined : Number(issue) }); log(describe(specStatus(cwd, path))); return; }
95
+ if (sub === 'approve') { approveSpec(cwd, path, { approver: flag(args, '--by') }); log(describe(specStatus(cwd, path))); return; }
96
+ const { ok, reasons } = evaluateApprovedSpecGate(cwd, path);
97
+ if (!ok) throw new Error('Spec gate closed for ' + path + ': ' + reasons.join('; '));
98
+ log('spec gate open: ' + path);
99
+ return;
100
+ }
101
+ if (command === 'check') {
102
+ const specProblems = checkSpecRecords(cwd).problems;
103
+ for (const p of specProblems) log('problem: ' + p);
104
+ const { ok, problems } = checkProject(cwd);
105
+ for (const p of problems) log('problem: ' + p);
106
+ if (!ok || specProblems.length) throw new Error(problems.length + specProblems.length + ' workflow problem(s)');
107
+ log('workflow docs ok');
108
+ return;
109
+ }
110
+ throw new Error('Usage: aether <init|doctor|status|install <kit>...|check|spec ...|run <pack>|run init <dir>|room [serve|snapshot|html <file>|install-vscode]|owner ...|idea ...|githooks <install|uninstall|status>|setup [--yes|--kits a,b] [--os-name ..] [--assistant ..] [--accent #hex] [--dry-run]|setup machine [--install [--yes]]|doctor machine|version|sprint <status|plan|close>|voice [--ai provider] [--model id|--tier t] [--agents [--agent id]] [--pack dir]|workspace [init] [--no-memory] [--no-price-refresh]|models [list|pick --tier cheap|balanced|strong|init|prices [update]]>');
111
+ }
112
+ if (isMain && process.argv[2] === 'sprint') {
113
+ runSprint(process.argv.slice(3), process.cwd(), console.log).catch(error => { console.error(error.message); process.exitCode = 1; });
114
+ } else if (isMain && process.argv[2] === 'setup') {
115
+ (process.argv[3] === 'machine'
116
+ ? (async () => { const rl = createInterface({ input: process.stdin, output: process.stdout }); try { await setupMachine({ install: process.argv.includes('--install'), yes: process.argv.includes('--yes'), ask: q => rl.question(q), log: console.log }); } finally { rl.close(); } })()
117
+ : setup(process.argv.slice(3), process.cwd(), console.log, { installers: INSTALLERS })).catch(error => { console.error(error.message); process.exitCode = 1; });
118
+ } else if (isMain && ['run', 'room', 'voice', 'workspace', 'models'].includes(process.argv[2])) {
119
+ import({ run: '@aetheros/runner/cli', room: '@aetheros/control-room/cli', voice: '@aetheros/voice/cli', workspace: '@aetheros/workspace/cli', models: '@aetheros/models/cli' }[process.argv[2]]).then(m => m.run(process.argv.slice(3))).then(code => { process.exitCode = code; }, error => { console.error(error.message); process.exitCode = 1; });
120
+ } else if (isMain) {
121
+ try { run(process.argv.slice(2)); } catch (error) { console.error(error.message); process.exitCode = 1; }
122
+ }
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@aetheros/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Abigshana/AetherOS.git",
8
+ "directory": "packages/cli"
9
+ },
10
+ "type": "module",
11
+ "bin": {
12
+ "aether": "./bin/aether.js"
13
+ },
14
+ "scripts": {
15
+ "check": "node --check bin/aether.js && node --check src/setup.js && node --check src/sprint.js && node --check src/machine.js",
16
+ "test": "node --test test/*.test.mjs"
17
+ },
18
+ "dependencies": {
19
+ "@aetheros/workflow": "0.1.0",
20
+ "@aetheros/provider-claude": "0.1.0",
21
+ "@aetheros/provider-openai": "0.1.0",
22
+ "@aetheros/spec-kit": "0.1.0",
23
+ "@aetheros/runner": "0.1.0",
24
+ "@aetheros/control-room": "0.1.0",
25
+ "@aetheros/voice": "0.1.0",
26
+ "@aetheros/workspace": "0.1.0",
27
+ "@aetheros/models": "0.1.0",
28
+ "@aetheros/provider-google": "0.1.0",
29
+ "@aetheros/provider-openai-compatible": "0.1.0"
30
+ },
31
+ "files": [
32
+ "src",
33
+ "dist",
34
+ "bin",
35
+ "templates",
36
+ "kits",
37
+ "README.md"
38
+ ]
39
+ }
package/src/machine.js ADDED
@@ -0,0 +1,41 @@
1
+ import {execFileSync} from 'node:child_process';
2
+ import {checkMachine} from './setup.js';
3
+
4
+ /** Install commands per platform. Linux is listed only (package managers and sudo differ), never run automatically. */
5
+ const PLANS = {
6
+ win32: {git: ['winget', ['install', '-e', '--id', 'Git.Git']], gh: ['winget', ['install', '-e', '--id', 'GitHub.cli']], code: ['winget', ['install', '-e', '--id', 'Microsoft.VisualStudioCode']], node: ['winget', ['install', '-e', '--id', 'OpenJS.NodeJS.LTS']]},
7
+ darwin: {git: ['brew', ['install', 'git']], gh: ['brew', ['install', 'gh']], code: ['brew', ['install', '--cask', 'visual-studio-code']], node: ['brew', ['install', 'node']]},
8
+ linux: {git: ['sudo', ['apt-get', 'install', '-y', 'git']], gh: ['sudo', ['apt-get', 'install', '-y', 'gh']], code: ['sudo', ['snap', 'install', 'code', '--classic']], node: ['sudo', ['apt-get', 'install', '-y', 'nodejs']]}
9
+ };
10
+ const CLAUDE = ['npm', ['install', '-g', '@anthropic-ai/claude-code']];
11
+ const EXTENSION = ['code', ['--install-extension', 'anthropic.claude-code']];
12
+
13
+ export function machinePlan({platform = process.platform, check = checkMachine()} = {}) {
14
+ const table = PLANS[platform] ?? {};
15
+ const steps = [];
16
+ for (const r of check.results) {
17
+ if (r.ok || r.name === 'npm') continue;
18
+ const cmd = r.name === 'claude' ? CLAUDE : table[r.name];
19
+ if (cmd) steps.push({tool: r.name, command: cmd[0], args: cmd[1], required: r.required});
20
+ }
21
+ const codeReady = check.results.find(r => r.name === 'code')?.ok || steps.some(s => s.tool === 'code');
22
+ if (codeReady) steps.push({tool: 'claude-code-extension', command: EXTENSION[0], args: EXTENSION[1], required: false});
23
+ return {platform, automatic: platform === 'win32' || platform === 'darwin', steps};
24
+ }
25
+
26
+ /** Show the plan; with install: true run each step after asking (or all with yes: true). Never runs on Linux. */
27
+ export async function setupMachine({install = false, yes = false, ask, log, platform, check, run = (cmd, args) => execFileSync(cmd, args, {stdio: 'inherit', shell: process.platform === 'win32'})} = {}) {
28
+ const plan = machinePlan({platform, check});
29
+ if (!plan.steps.length) { log('Nothing to install: this computer has every tool AetherOS uses.'); return {plan, ran: []}; }
30
+ for (const s of plan.steps) log(`${s.required ? 'needed ' : 'optional'} ${s.tool}: ${s.command} ${s.args.join(' ')}`);
31
+ if (!install) { log('Preview only. Run `aether setup machine --install` to install (you will be asked for each).'); return {plan, ran: []}; }
32
+ if (!plan.automatic) { log('Automatic install is only offered on Windows and macOS; run the commands above yourself.'); return {plan, ran: []}; }
33
+ const ran = [];
34
+ for (const s of plan.steps) {
35
+ if (!yes && !String(await ask(`Install ${s.tool}? [y/N] `)).trim().toLowerCase().startsWith('y')) continue;
36
+ run(s.command, s.args);
37
+ ran.push(s.tool);
38
+ }
39
+ log(ran.length ? `Installed: ${ran.join(', ')}. Open a new terminal, then run \`aether doctor machine\`.` : 'Nothing installed.');
40
+ return {plan, ran};
41
+ }
package/src/setup.js ADDED
@@ -0,0 +1,100 @@
1
+ import {execFileSync} from 'node:child_process';
2
+ import {existsSync, readFileSync, writeFileSync} from 'node:fs';
3
+ import {readBrand, writeBrand} from '@aetheros/workflow';
4
+ import {basename, resolve} from 'node:path';
5
+ import {createInterface} from 'node:readline/promises';
6
+
7
+ const KIT_ORDER = ['github', 'memory', 'claude', 'openai', 'githooks', 'mcp'];
8
+ const ABOUT = {
9
+ github: 'GitHub issue and pull request templates',
10
+ memory: 'Handover, roadmap, feature folders, workflow settings',
11
+ claude: 'Claude Code agents, skills and guard hooks',
12
+ openai: 'AGENTS.md for OpenAI tooling',
13
+ githooks: 'Git hooks guard rails for every tool and person',
14
+ mcp: 'Claude Code MCP servers (Playwright browser)'
15
+ };
16
+ const isGitRepo = root => { try { execFileSync('git', ['rev-parse', '--git-dir'], {cwd: root, stdio: 'ignore'}); return true; } catch { return false; } };
17
+
18
+ /**
19
+ * Guided project setup. Non-interactive with --yes (defaults) or --kits a,b,c; otherwise asks one question per kit.
20
+ * installers: {kit: (root, {dryRun}) => {created, skipped}}. Never overwrites existing files.
21
+ */
22
+ export async function setup(args, cwd, log, {installers, ask, interactive = Boolean(process.stdin.isTTY)} = {}) {
23
+ const root = resolve(cwd);
24
+ const dryRun = args.includes('--dry-run');
25
+ const git = isGitRepo(root);
26
+ const defaults = KIT_ORDER.filter(k => k !== 'mcp' && (k !== 'githooks' || git));
27
+ const listed = args.includes('--kits') ? (args[args.indexOf('--kits') + 1] || '').split(',').map(s => s.trim()).filter(Boolean) : null;
28
+ if (listed && listed.some(k => !installers[k])) throw new Error('Unknown kit in --kits; available: ' + KIT_ORDER.join(', '));
29
+ if (listed?.includes('githooks') && !git) throw new Error('githooks needs a git repository (run git init first)');
30
+ let kits = listed ?? defaults;
31
+ if (!listed && !args.includes('--yes') && interactive) {
32
+ const rl = ask ? null : createInterface({input: process.stdin, output: process.stdout});
33
+ const question = ask ?? (q => rl.question(q));
34
+ kits = [];
35
+ try {
36
+ for (const kit of KIT_ORDER) {
37
+ if (kit === 'githooks' && !git) continue;
38
+ const def = defaults.includes(kit);
39
+ const answer = String(await question(`Install ${kit} (${ABOUT[kit]})? ${def ? '[Y/n]' : '[y/N]'} `)).trim().toLowerCase();
40
+ if (answer ? answer.startsWith('y') : def) kits.push(kit);
41
+ }
42
+ } finally { rl?.close(); }
43
+ }
44
+ // Brand: the product's OS name, assistant name and accent colour (e.g. "Nova OS", "Nova").
45
+ const flag = name => (args.includes(name) ? args[args.indexOf(name) + 1] : undefined);
46
+ let brand = readBrand(root);
47
+ let osName = flag('--os-name'), assistantName = flag('--assistant'), accent = flag('--accent');
48
+ if (!brand && !osName && !args.includes('--yes') && interactive) {
49
+ const rl = ask ? null : createInterface({input: process.stdin, output: process.stdout});
50
+ const question = ask ?? (q => rl.question(q));
51
+ try {
52
+ osName = String(await question(`What is your OS called? [${basename(root)} OS] `)).trim() || `${basename(root)} OS`;
53
+ assistantName = String(await question('What is your assistant called? [Aevron] ')).trim() || 'Aevron';
54
+ accent = String(await question('Accent colour (#rrggbb, empty for default)? ')).trim() || undefined;
55
+ } finally { rl?.close(); }
56
+ }
57
+ if (osName || assistantName) {
58
+ const next = {osName: osName || brand?.osName || `${basename(root)} OS`, assistantName: assistantName || brand?.assistantName || 'Aevron', theme: {...(brand?.theme ?? {}), ...(accent && {accent})}};
59
+ if (!dryRun) brand = writeBrand(root, next);
60
+ log(`${dryRun ? 'would brand' : 'branded'}: ${next.osName} · assistant ${next.assistantName}`);
61
+ }
62
+ const hadClaudeMd = existsSync(resolve(root, 'CLAUDE.md'));
63
+ const config = resolve(root, 'aether.config.json');
64
+ if (!existsSync(config)) {
65
+ if (!dryRun) writeFileSync(config, JSON.stringify({schemaVersion: 1, project: {name: basename(root)}, providers: ['claude', 'openai'], approval: 'writes'}, null, 2) + '\n', {flag: 'wx'});
66
+ log((dryRun ? 'would create ' : 'created ') + 'aether.config.json');
67
+ }
68
+ for (const kit of kits) {
69
+ const {created, skipped} = installers[kit](root, {dryRun});
70
+ log(`${kit}: ${created.length} ${dryRun ? 'to create' : 'created'}, ${skipped.length} kept`);
71
+ }
72
+ if (brand && !dryRun && !hadClaudeMd && existsSync(resolve(root, 'CLAUDE.md'))) {
73
+ const file = resolve(root, 'CLAUDE.md');
74
+ writeFileSync(file, readFileSync(file, 'utf8').replace(/^# Project instructions/m, `# ${brand.osName} — project instructions\n\nThe AI assistant for this project is called ${brand.assistantName}.`));
75
+ }
76
+ log('Next: `aether doctor machine` (tools on this computer), `aether check`, `aether room`, `aether run init packs/<app>`.');
77
+ return kits;
78
+ }
79
+
80
+ const TOOLS = [
81
+ {name: 'node', args: ['--version'], required: true, min: 20, hint: 'https://nodejs.org (version 20 or newer)'},
82
+ {name: 'git', args: ['--version'], required: true, hint: 'https://git-scm.com'},
83
+ {name: 'npm', args: ['--version'], required: true, hint: 'comes with Node.js'},
84
+ {name: 'gh', args: ['--version'], hint: 'https://cli.github.com (GitHub from the terminal)'},
85
+ {name: 'claude', args: ['--version'], hint: 'https://claude.com/claude-code (Claude Code CLI)'},
86
+ {name: 'code', args: ['--version'], hint: 'https://code.visualstudio.com (for the Control Room panel)'}
87
+ ];
88
+
89
+ /** Check the tools AetherOS uses on this computer. Never installs anything. */
90
+ export function checkMachine({run = (cmd, args) => execFileSync(cmd, args, {encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 10000, shell: process.platform === 'win32'}), nodeVersion = process.versions.node} = {}) {
91
+ const results = TOOLS.map(tool => {
92
+ try {
93
+ const out = tool.name === 'node' ? 'v' + nodeVersion : run(tool.name, tool.args);
94
+ const version = (/\d+\.\d+(\.\d+)?/.exec(out) || [''])[0];
95
+ const tooOld = tool.min && Number(version.split('.')[0]) < tool.min;
96
+ return {name: tool.name, ok: !tooOld, required: Boolean(tool.required), version, hint: tooOld ? tool.hint : undefined};
97
+ } catch { return {name: tool.name, ok: false, required: Boolean(tool.required), hint: tool.hint}; }
98
+ });
99
+ return {ok: results.every(r => r.ok || !r.required), results};
100
+ }
package/src/sprint.js ADDED
@@ -0,0 +1,31 @@
1
+ import {closeSprint, githubClient, planSprint, repoFromGit, sprintStatus} from '@aetheros/workflow';
2
+
3
+ const value = (args, name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
4
+
5
+ /** aether sprint status | plan --title "Sprint N — goal" --issues 1,2 [--due YYYY-MM-DD] | close [--next <milestone>] */
6
+ export async function runSprint(args, cwd, log, {token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN, fetchImpl} = {}) {
7
+ const sub = args[0] || 'status';
8
+ const repo = value(args, '--repo') || repoFromGit(cwd);
9
+ if (!repo) throw new Error('Could not find the GitHub repository; pass --repo owner/name');
10
+ const client = githubClient({token, fetchImpl});
11
+ if (sub === 'status') {
12
+ const s = await sprintStatus(client, repo);
13
+ if (!s) { log('No open sprint. Plan one with: aether sprint plan --title "Sprint 1 — goal" --issues 1,2,3'); return; }
14
+ log(`${s.title}${s.dueOn ? ' (due ' + s.dueOn.slice(0, 10) + ')' : ''}: ${s.points.done} points done, ${s.points.open} open`);
15
+ for (const i of s.open) log(` #${i.number} ${i.title}${i.points ? ` [${i.points}]` : ''}${i.blocked ? ' — waiting on the owner' : ''}`);
16
+ return;
17
+ }
18
+ if (sub === 'plan') {
19
+ const issues = (value(args, '--issues') || '').split(',').map(n => Number(n.trim().replace(/^#/, ''))).filter(Boolean);
20
+ const r = await planSprint(client, repo, {title: value(args, '--title'), dueOn: value(args, '--due'), issues});
21
+ log(`created ${r.title} (milestone ${r.number}) with ${r.issues.length} issue(s), ${r.points} points`);
22
+ return;
23
+ }
24
+ if (sub === 'close') {
25
+ const next = value(args, '--next');
26
+ const r = await closeSprint(client, repo, {next: next ? Number(next) : undefined});
27
+ log(`closed ${r.title}: ${r.points.done} points done; moved ${r.moved.length} open issue(s) to ${r.movedTo === 'backlog' ? 'the backlog' : 'milestone ' + r.movedTo}`);
28
+ return;
29
+ }
30
+ throw new Error('Usage: aether sprint <status|plan --title "Sprint N — goal" --issues 1,2 [--due YYYY-MM-DD]|close [--next <milestone number>]> [--repo owner/name]');
31
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.