@lumoai/cli 1.0.0 → 1.2.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/README.md +37 -7
- package/assets/skill.md +823 -0
- package/dist/cli/src/commands/setup.js +161 -0
- package/dist/cli/src/commands/update.js +71 -0
- package/dist/cli/src/index.js +39 -5
- package/dist/cli/src/lib/hooks-template.js +72 -0
- package/dist/cli/src/lib/line-prompt.js +36 -0
- package/dist/cli/src/lib/update-check.js +148 -0
- package/package.json +6 -5
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.setup = setup;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
const hooks_template_1 = require("../lib/hooks-template");
|
|
42
|
+
const line_prompt_1 = require("../lib/line-prompt");
|
|
43
|
+
async function setup(options) {
|
|
44
|
+
if (options.user && options.project) {
|
|
45
|
+
process.stderr.write('Error: --user and --project are mutually exclusive.\n');
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
const scope = await resolveScope(options);
|
|
49
|
+
if (!scope)
|
|
50
|
+
return 130;
|
|
51
|
+
const root = scope === 'user' ? os.homedir() : process.cwd();
|
|
52
|
+
const claudeDir = path.join(root, '.claude');
|
|
53
|
+
process.stdout.write(`\nInstalling Lumo for ${scope} scope: ${claudeDir}\n\n`);
|
|
54
|
+
installSkill(claudeDir, options.force === true);
|
|
55
|
+
mergeSettings(claudeDir);
|
|
56
|
+
printPostInstall();
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
async function resolveScope(options) {
|
|
60
|
+
if (options.user)
|
|
61
|
+
return 'user';
|
|
62
|
+
if (options.project)
|
|
63
|
+
return 'project';
|
|
64
|
+
if (!process.stdin.isTTY) {
|
|
65
|
+
process.stdout.write('No --user/--project flag given and stdin is not a TTY. Defaulting to --project.\n');
|
|
66
|
+
return 'project';
|
|
67
|
+
}
|
|
68
|
+
const answer = (await (0, line_prompt_1.promptLine)('Install Lumo for [u]ser (~/.claude) or [p]roject (./.claude)? [u/P]: ')).toLowerCase();
|
|
69
|
+
if (answer === 'u' || answer === 'user')
|
|
70
|
+
return 'user';
|
|
71
|
+
if (answer === '' || answer === 'p' || answer === 'project')
|
|
72
|
+
return 'project';
|
|
73
|
+
process.stderr.write(`Unrecognized choice "${answer}". Aborting.\n`);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function installSkill(claudeDir, force) {
|
|
77
|
+
const skillDir = path.join(claudeDir, 'skills', 'lumo');
|
|
78
|
+
const skillDst = path.join(skillDir, 'SKILL.md');
|
|
79
|
+
// Asset bundled in cli/assets/skill.md, shipped via package.json "files".
|
|
80
|
+
// From dist/cli/src/commands/setup.js → ../../../../assets/skill.md
|
|
81
|
+
const skillSrc = path.resolve(__dirname, '../../../..', 'assets', 'skill.md');
|
|
82
|
+
if (!fs.existsSync(skillSrc)) {
|
|
83
|
+
throw new Error(`Bundled skill asset missing at ${skillSrc} — reinstall @lumoai/cli.`);
|
|
84
|
+
}
|
|
85
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
86
|
+
if (fs.existsSync(skillDst) && !force) {
|
|
87
|
+
const srcStat = fs.statSync(skillSrc);
|
|
88
|
+
const dstStat = fs.statSync(skillDst);
|
|
89
|
+
if (srcStat.size === dstStat.size) {
|
|
90
|
+
const a = fs.readFileSync(skillSrc, 'utf8');
|
|
91
|
+
const b = fs.readFileSync(skillDst, 'utf8');
|
|
92
|
+
if (a === b) {
|
|
93
|
+
process.stdout.write(`✓ skill already up to date: ${skillDst}\n`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
process.stdout.write(`⚠ ${skillDst} exists and differs from the bundled version.\n` +
|
|
98
|
+
` Re-run with --force to overwrite.\n`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
fs.copyFileSync(skillSrc, skillDst);
|
|
102
|
+
process.stdout.write(`✓ wrote skill: ${skillDst}\n`);
|
|
103
|
+
}
|
|
104
|
+
function mergeSettings(claudeDir) {
|
|
105
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
106
|
+
let existing = {};
|
|
107
|
+
if (fs.existsSync(settingsPath)) {
|
|
108
|
+
try {
|
|
109
|
+
existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
113
|
+
throw new Error(`Cannot parse existing ${settingsPath}: ${msg}. Fix the JSON and re-run.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
const { merged, stats } = (0, hooks_template_1.mergeLumoHooks)(existing);
|
|
120
|
+
fs.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n');
|
|
121
|
+
if (stats.addedEvents.length === 0) {
|
|
122
|
+
process.stdout.write(`✓ hooks already wired in: ${settingsPath} (${stats.alreadyPresent.length} events)\n`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
process.stdout.write(`✓ merged hooks into ${settingsPath} (added ${stats.addedEvents.length}, kept ${stats.alreadyPresent.length})\n`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function printPostInstall() {
|
|
129
|
+
const onPath = isLumoOnPath();
|
|
130
|
+
const credsPath = path.join(process.env.LUMO_CONFIG_DIR || path.join(os.homedir(), '.lumo'), 'credentials.json');
|
|
131
|
+
const authed = fs.existsSync(credsPath);
|
|
132
|
+
process.stdout.write('\nNext steps:\n');
|
|
133
|
+
if (!onPath || isRunningUnderNpx()) {
|
|
134
|
+
process.stdout.write(' • Install the CLI globally so Claude Code hooks can find it:\n' +
|
|
135
|
+
' npm install -g @lumoai/cli\n');
|
|
136
|
+
}
|
|
137
|
+
if (!authed) {
|
|
138
|
+
process.stdout.write(' • Authenticate so the CLI can sync with the Lumo server:\n' +
|
|
139
|
+
' lumo auth login\n');
|
|
140
|
+
}
|
|
141
|
+
if (onPath && authed && !isRunningUnderNpx()) {
|
|
142
|
+
process.stdout.write(' • All set. Open Claude Code in this directory — the SKILL.md and\n' +
|
|
143
|
+
' hooks are wired in.\n');
|
|
144
|
+
}
|
|
145
|
+
process.stdout.write('\n');
|
|
146
|
+
}
|
|
147
|
+
function isLumoOnPath() {
|
|
148
|
+
try {
|
|
149
|
+
// `command -v` is a POSIX shell builtin (`execSync` defaults to /bin/sh
|
|
150
|
+
// on Unix). `where` is a cmd.exe builtin on Windows.
|
|
151
|
+
(0, child_process_1.execSync)(process.platform === 'win32' ? 'where lumo' : 'command -v lumo', { stdio: 'pipe' });
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function isRunningUnderNpx() {
|
|
159
|
+
const argv1 = process.argv[1] || '';
|
|
160
|
+
return /[\\/]_npx[\\/]/.test(argv1) || process.env.npm_command === 'exec';
|
|
161
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.update = update;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const update_check_1 = require("../lib/update-check");
|
|
40
|
+
async function update() {
|
|
41
|
+
const pkg = require(path.resolve(__dirname, '../../../..', 'package.json'));
|
|
42
|
+
process.stdout.write(`Current: ${pkg.name}@${pkg.version}\n`);
|
|
43
|
+
process.stdout.write('Checking npm registry...\n');
|
|
44
|
+
const latest = await (0, update_check_1.fetchLatestVersion)(pkg.name, 5000);
|
|
45
|
+
if (!latest) {
|
|
46
|
+
process.stderr.write('Could not reach npm registry. Check your connection and try again.\n');
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
if (latest === pkg.version) {
|
|
50
|
+
process.stdout.write(`Already on the latest version (${latest}).\n`);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
if (!(0, update_check_1.isNewer)(pkg.version, latest)) {
|
|
54
|
+
process.stdout.write(`Installed version (${pkg.version}) is ahead of the published latest (${latest}). Nothing to do.\n`);
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
process.stdout.write(`Latest: ${pkg.name}@${latest}\n\n`);
|
|
58
|
+
process.stdout.write(`Running: npm install -g ${pkg.name}@latest\n\n`);
|
|
59
|
+
return new Promise(resolve => {
|
|
60
|
+
const proc = (0, child_process_1.spawn)('npm', ['install', '-g', `${pkg.name}@latest`], {
|
|
61
|
+
stdio: 'inherit',
|
|
62
|
+
shell: process.platform === 'win32',
|
|
63
|
+
});
|
|
64
|
+
proc.on('close', code => resolve(code ?? 0));
|
|
65
|
+
proc.on('error', err => {
|
|
66
|
+
process.stderr.write(`Failed to launch npm: ${err.message}\n`);
|
|
67
|
+
process.stderr.write(`Fallback: run \`npm install -g ${pkg.name}@latest\` manually.\n`);
|
|
68
|
+
resolve(1);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -78,9 +78,30 @@ const doc_share_1 = require("./commands/doc-share");
|
|
|
78
78
|
const doc_unshare_1 = require("./commands/doc-unshare");
|
|
79
79
|
const doc_share_list_1 = require("./commands/doc-share-list");
|
|
80
80
|
const doc_move_1 = require("./commands/doc-move");
|
|
81
|
+
const update_1 = require("./commands/update");
|
|
82
|
+
const setup_1 = require("./commands/setup");
|
|
83
|
+
const update_check_1 = require("./lib/update-check");
|
|
81
84
|
// Resolve package.json relative to __dirname so this works regardless of how
|
|
82
85
|
// deep the compiled output ends up (flat dist/ or nested dist/cli/src/).
|
|
83
86
|
const pkg = require(path.resolve(__dirname, '../../..', 'package.json'));
|
|
87
|
+
// Detached background-refresh worker: re-entry point for the spawn() in
|
|
88
|
+
// maybeRefreshInBackground(). Fetches latest, writes cache, exits — skipping
|
|
89
|
+
// commander.parseAsync() below.
|
|
90
|
+
const isUpdateCheckWorker = process.argv[2] === '__update-check' && !!process.argv[3];
|
|
91
|
+
if (isUpdateCheckWorker) {
|
|
92
|
+
(0, update_check_1.runBackgroundRefresh)(process.argv[3])
|
|
93
|
+
.catch(() => { })
|
|
94
|
+
.finally(() => process.exit(0));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
(0, update_check_1.printUpdateNoticeIfAny)(pkg.name, pkg.version);
|
|
98
|
+
(0, update_check_1.maybeRefreshInBackground)(pkg.name);
|
|
99
|
+
}
|
|
100
|
+
// One-shot cleanup of pre-LUM-46 local state. The session→task binding
|
|
101
|
+
// is now server-authoritative; the legacy global pointer and the
|
|
102
|
+
// per-session sentinel directory are both obsolete. Failures are
|
|
103
|
+
// swallowed so a permission glitch doesn't prevent CLI invocation.
|
|
104
|
+
;
|
|
84
105
|
(() => {
|
|
85
106
|
const dir = process.env.LUMO_CONFIG_DIR || path.join(os.homedir(), '.lumo');
|
|
86
107
|
try {
|
|
@@ -124,6 +145,17 @@ program
|
|
|
124
145
|
.command('whoami')
|
|
125
146
|
.description('Show the currently logged-in identity')
|
|
126
147
|
.action(wrap(whoami_1.whoami));
|
|
148
|
+
program
|
|
149
|
+
.command('update')
|
|
150
|
+
.description(`Check npm for a newer ${pkg.name} and install it globally if available.`)
|
|
151
|
+
.action(wrap(update_1.update));
|
|
152
|
+
program
|
|
153
|
+
.command('setup')
|
|
154
|
+
.description('Install the Lumo Claude Code skill and wire hook handlers into .claude/settings.json. Run via `npx @lumoai/cli setup` for first-time onboarding.')
|
|
155
|
+
.option('--user', 'Install into ~/.claude (applies across all projects for this user)')
|
|
156
|
+
.option('--project', 'Install into ./.claude (applies to the current project only)')
|
|
157
|
+
.option('--force', 'Overwrite an existing SKILL.md when its contents differ from the bundled version')
|
|
158
|
+
.action(wrap(options => (0, setup_1.setup)(options)));
|
|
127
159
|
const session = program
|
|
128
160
|
.command('session')
|
|
129
161
|
.description('Manage per-terminal coding-session context');
|
|
@@ -485,8 +517,10 @@ hook
|
|
|
485
517
|
.command('instructions-loaded')
|
|
486
518
|
.description('Forward an InstructionsLoaded hook event to Lumo (reads JSON from stdin)')
|
|
487
519
|
.action(wrap(() => (0, hook_1.hookCommand)('instructions-loaded')));
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
520
|
+
if (!isUpdateCheckWorker) {
|
|
521
|
+
program.parseAsync(process.argv).catch(err => {
|
|
522
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
523
|
+
console.error(`Error: ${msg}`);
|
|
524
|
+
process.exit(1);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LUMO_HOOK_EVENTS = void 0;
|
|
4
|
+
exports.buildLumoHookFragment = buildLumoHookFragment;
|
|
5
|
+
exports.mergeLumoHooks = mergeLumoHooks;
|
|
6
|
+
// Canonical list of Claude Code hook events that the Lumo CLI handles via
|
|
7
|
+
// `lumo hook <event>`. Each entry maps a Claude Code event name (CamelCase,
|
|
8
|
+
// matches settings.json keys) to the slug-form subcommand. Kept in sync with
|
|
9
|
+
// the `hook` subcommand registrations in cli/src/index.ts and with the
|
|
10
|
+
// repository's own .claude/settings.json.
|
|
11
|
+
exports.LUMO_HOOK_EVENTS = [
|
|
12
|
+
['PreToolUse', 'pre-tool-use'],
|
|
13
|
+
['PostToolUse', 'post-tool-use'],
|
|
14
|
+
['PostToolUseFailure', 'post-tool-use-failure'],
|
|
15
|
+
['UserPromptSubmit', 'user-prompt-submit'],
|
|
16
|
+
['Stop', 'stop'],
|
|
17
|
+
['StopFailure', 'stop-failure'],
|
|
18
|
+
['PermissionRequest', 'permission-request'],
|
|
19
|
+
['PermissionDenied', 'permission-denied'],
|
|
20
|
+
['SessionStart', 'session-start'],
|
|
21
|
+
['SessionEnd', 'session-end'],
|
|
22
|
+
['SubagentStart', 'subagent-start'],
|
|
23
|
+
['SubagentStop', 'subagent-stop'],
|
|
24
|
+
['WorktreeCreate', 'worktree-create'],
|
|
25
|
+
['WorktreeRemove', 'worktree-remove'],
|
|
26
|
+
['FileChanged', 'file-changed'],
|
|
27
|
+
['ConfigChange', 'config-change'],
|
|
28
|
+
['TaskCreated', 'task-created'],
|
|
29
|
+
['TaskCompleted', 'task-completed'],
|
|
30
|
+
['PostToolBatch', 'post-tool-batch'],
|
|
31
|
+
['UserPromptExpansion', 'user-prompt-expansion'],
|
|
32
|
+
['Notification', 'notification'],
|
|
33
|
+
['Elicitation', 'elicitation'],
|
|
34
|
+
['ElicitationResult', 'elicitation-result'],
|
|
35
|
+
['CwdChanged', 'cwd-changed'],
|
|
36
|
+
['InstructionsLoaded', 'instructions-loaded'],
|
|
37
|
+
];
|
|
38
|
+
// Build the settings.json fragment we want to install. We do not use a
|
|
39
|
+
// matcher because the Lumo hook handlers ingest every event of a given type;
|
|
40
|
+
// adding a matcher would silently drop events that have no `tool_name` (e.g.
|
|
41
|
+
// SessionStart). Keep this in step with cli/src/commands/hook.ts dispatch.
|
|
42
|
+
function buildLumoHookFragment() {
|
|
43
|
+
const hooks = {};
|
|
44
|
+
for (const [event, slug] of exports.LUMO_HOOK_EVENTS) {
|
|
45
|
+
hooks[event] = [
|
|
46
|
+
{ hooks: [{ type: 'command', command: `lumo hook ${slug}` }] },
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
return { hooks };
|
|
50
|
+
}
|
|
51
|
+
function mergeLumoHooks(existing) {
|
|
52
|
+
const base = existing ? JSON.parse(JSON.stringify(existing)) : {};
|
|
53
|
+
if (!base.hooks)
|
|
54
|
+
base.hooks = {};
|
|
55
|
+
const stats = { addedEvents: [], alreadyPresent: [] };
|
|
56
|
+
for (const [event, slug] of exports.LUMO_HOOK_EVENTS) {
|
|
57
|
+
const ourCmd = `lumo hook ${slug}`;
|
|
58
|
+
const groups = base.hooks[event] ?? [];
|
|
59
|
+
const present = groups.some(g => Array.isArray(g.hooks) && g.hooks.some(h => h.command === ourCmd));
|
|
60
|
+
if (present) {
|
|
61
|
+
stats.alreadyPresent.push(event);
|
|
62
|
+
base.hooks[event] = groups;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
base.hooks[event] = [
|
|
66
|
+
...groups,
|
|
67
|
+
{ hooks: [{ type: 'command', command: ourCmd }] },
|
|
68
|
+
];
|
|
69
|
+
stats.addedEvents.push(event);
|
|
70
|
+
}
|
|
71
|
+
return { merged: base, stats };
|
|
72
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.promptLine = promptLine;
|
|
4
|
+
// Plain-text line prompt for interactive choices. Echoes input (unlike
|
|
5
|
+
// promptSecret). Returns the trimmed line. Resolves with an empty string when
|
|
6
|
+
// stdin ends before a newline (callers should fall back to a default).
|
|
7
|
+
function promptLine(message) {
|
|
8
|
+
return new Promise(resolve => {
|
|
9
|
+
const stdin = process.stdin;
|
|
10
|
+
if (stdin.isTTY) {
|
|
11
|
+
process.stdout.write(message);
|
|
12
|
+
}
|
|
13
|
+
stdin.resume();
|
|
14
|
+
stdin.setEncoding('utf8');
|
|
15
|
+
let buf = '';
|
|
16
|
+
const onData = (chunk) => {
|
|
17
|
+
buf += chunk;
|
|
18
|
+
const nl = buf.indexOf('\n');
|
|
19
|
+
if (nl === -1)
|
|
20
|
+
return;
|
|
21
|
+
cleanup();
|
|
22
|
+
resolve(buf.slice(0, nl).replace(/\r$/, '').trim());
|
|
23
|
+
};
|
|
24
|
+
const onEnd = () => {
|
|
25
|
+
cleanup();
|
|
26
|
+
resolve(buf.trim());
|
|
27
|
+
};
|
|
28
|
+
const cleanup = () => {
|
|
29
|
+
stdin.removeListener('data', onData);
|
|
30
|
+
stdin.removeListener('end', onEnd);
|
|
31
|
+
stdin.pause();
|
|
32
|
+
};
|
|
33
|
+
stdin.on('data', onData);
|
|
34
|
+
stdin.on('end', onEnd);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isNewer = isNewer;
|
|
37
|
+
exports.fetchLatestVersion = fetchLatestVersion;
|
|
38
|
+
exports.printUpdateNoticeIfAny = printUpdateNoticeIfAny;
|
|
39
|
+
exports.maybeRefreshInBackground = maybeRefreshInBackground;
|
|
40
|
+
exports.runBackgroundRefresh = runBackgroundRefresh;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const https = __importStar(require("https"));
|
|
45
|
+
const child_process_1 = require("child_process");
|
|
46
|
+
const CACHE_FILE = path.join(process.env.LUMO_CONFIG_DIR || path.join(os.homedir(), '.lumo'), 'update-check.json');
|
|
47
|
+
const CHECK_INTERVAL_MS = 1000 * 60 * 60 * 24;
|
|
48
|
+
function readCache() {
|
|
49
|
+
try {
|
|
50
|
+
const raw = fs.readFileSync(CACHE_FILE, 'utf8');
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
if (typeof parsed.lastChecked !== 'number' ||
|
|
53
|
+
typeof parsed.latest !== 'string')
|
|
54
|
+
return null;
|
|
55
|
+
return parsed;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function writeCache(cache) {
|
|
62
|
+
try {
|
|
63
|
+
fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
|
|
64
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* swallow — the update check is non-essential */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Compare two dotted version strings. Returns true iff `b` is strictly newer.
|
|
71
|
+
// Handles the subset of semver we actually publish: MAJOR.MINOR.PATCH with
|
|
72
|
+
// no pre-release tags. Unknown segments are treated as 0.
|
|
73
|
+
function isNewer(current, candidate) {
|
|
74
|
+
const c = current.split('.').map(n => parseInt(n, 10) || 0);
|
|
75
|
+
const l = candidate.split('.').map(n => parseInt(n, 10) || 0);
|
|
76
|
+
for (let i = 0; i < 3; i++) {
|
|
77
|
+
const cv = c[i] ?? 0;
|
|
78
|
+
const lv = l[i] ?? 0;
|
|
79
|
+
if (lv > cv)
|
|
80
|
+
return true;
|
|
81
|
+
if (lv < cv)
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
function fetchLatestVersion(name, timeoutMs = 2000) {
|
|
87
|
+
return new Promise(resolve => {
|
|
88
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(name).replace('%40', '@')}/latest`;
|
|
89
|
+
const req = https.get(url, { headers: { Accept: 'application/json' } }, res => {
|
|
90
|
+
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
91
|
+
res.resume();
|
|
92
|
+
return resolve(null);
|
|
93
|
+
}
|
|
94
|
+
let data = '';
|
|
95
|
+
res.setEncoding('utf8');
|
|
96
|
+
res.on('data', chunk => {
|
|
97
|
+
data += chunk;
|
|
98
|
+
});
|
|
99
|
+
res.on('end', () => {
|
|
100
|
+
try {
|
|
101
|
+
const json = JSON.parse(data);
|
|
102
|
+
resolve(typeof json.version === 'string' ? json.version : null);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
resolve(null);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
req.on('error', () => resolve(null));
|
|
110
|
+
req.setTimeout(timeoutMs, () => {
|
|
111
|
+
req.destroy();
|
|
112
|
+
resolve(null);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// Read cached latest version and emit a one-line notice to stderr when a
|
|
117
|
+
// newer version is available. Safe to call on every CLI invocation.
|
|
118
|
+
function printUpdateNoticeIfAny(name, current) {
|
|
119
|
+
const cache = readCache();
|
|
120
|
+
if (!cache)
|
|
121
|
+
return;
|
|
122
|
+
if (isNewer(current, cache.latest)) {
|
|
123
|
+
process.stderr.write(`\n ⬆ Update available: ${name} ${current} → ${cache.latest}\n Run: lumo update\n\n`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Spawn a detached child to refresh the cached latest version when the cache
|
|
127
|
+
// is older than CHECK_INTERVAL_MS. The child re-enters the CLI entrypoint via
|
|
128
|
+
// the `__update-check` sentinel arg and exits silently when done. Failures
|
|
129
|
+
// here must never affect the foreground command.
|
|
130
|
+
function maybeRefreshInBackground(name) {
|
|
131
|
+
const cache = readCache();
|
|
132
|
+
if (cache && Date.now() - cache.lastChecked < CHECK_INTERVAL_MS)
|
|
133
|
+
return;
|
|
134
|
+
try {
|
|
135
|
+
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1] || '', '__update-check', name], { detached: true, stdio: 'ignore' });
|
|
136
|
+
child.unref();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
/* swallow */
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Entrypoint for the detached child. Called from index.ts when argv[2] is
|
|
143
|
+
// the sentinel; performs the fetch and writes the cache, then exits.
|
|
144
|
+
async function runBackgroundRefresh(name) {
|
|
145
|
+
const latest = await fetchLatestVersion(name, 5000);
|
|
146
|
+
if (latest)
|
|
147
|
+
writeCache({ lastChecked: Date.now(), latest });
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumoai/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Lumo CLI — manage tasks and sessions from the terminal",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "cli@uselumo.ai",
|
|
@@ -24,18 +24,19 @@
|
|
|
24
24
|
"node": ">=18"
|
|
25
25
|
},
|
|
26
26
|
"bin": {
|
|
27
|
-
"lumo": "
|
|
27
|
+
"lumo": "dist/cli/src/index.js"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
|
-
"dist"
|
|
30
|
+
"dist",
|
|
31
|
+
"assets"
|
|
31
32
|
],
|
|
32
33
|
"publishConfig": {
|
|
33
34
|
"access": "public"
|
|
34
35
|
},
|
|
35
36
|
"scripts": {
|
|
36
|
-
"build": "tsc && chmod +x dist/cli/src/index.js",
|
|
37
|
+
"build": "tsc && chmod +x dist/cli/src/index.js && node ./scripts/bundle-assets.js",
|
|
37
38
|
"dev": "tsc --watch",
|
|
38
|
-
"clean": "rm -rf dist",
|
|
39
|
+
"clean": "rm -rf dist assets",
|
|
39
40
|
"prepublishOnly": "npm run clean && npm run build"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|