• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nightconcept / almandine / 14846535458

05 May 2025 09:22PM UTC coverage: 32.421% (-35.5%) from 67.965%
14846535458

push

github

web-flow
fix: Change init module to be e2e testable (#17)

91 of 420 new or added lines in 11 files covered. (21.67%)

462 of 1425 relevant lines covered (32.42%)

1.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

43.83
/src/main.lua
1
---
2
--- Main Entrypoint for Almandine Package Manager
3
---
4
--- This file serves as the main entrypoint for the Almandine Lua package manager.
5
--- It is responsible for bootstrapping the application and delegating control to the appropriate
6
--- modules based on user input or CLI arguments.
7
--- All initialization and top-level logic begins here.
8
---
9

10
---
11
-- Entry point for the Almandine CLI application.
12
-- Parses CLI arguments and dispatches to the appropriate command module.
13
---
14
-- @usage lua src/main.lua <command> [options]
15
--
16

17
-- Robustly set package.path relative to this script's directory (cross-platform)
18
local function script_dir()
19
  local info = debug.getinfo(1, "S").source
2✔
20
  local path = info:sub(1, 1) == "@" and info:sub(2) or info
2✔
21
  -- Normalize path separators for Windows
22
  path = path:gsub("\\", "/")
2✔
23
  return path:match("(.*/)") or "./"
2✔
24
end
25

26
local dir = script_dir()
2✔
27
if not package.path:find(dir .. "?.lua", 1, true) then
2✔
28
  package.path = dir .. "?.lua;" .. package.path
×
29
end
30
if not package.path:find(dir .. "?/init.lua", 1, true) then
2✔
31
  package.path = dir .. "?/init.lua;" .. package.path
×
32
end
33
if not package.path:find(dir .. "lib/?.lua", 1, true) then
2✔
34
  package.path = dir .. "lib/?.lua;" .. package.path
1✔
35
end
36

37
local unpack = table.unpack or unpack
2✔
38

39
local downloader = require("utils.downloader")
2✔
40
local manifest_utils = require("utils.manifest")
2✔
41
local init_module = require("modules.init")
2✔
42
local add_module = require("modules.add")
2✔
43
local install_module = require("modules.install")
2✔
44
local remove_module = require("modules.remove")
2✔
45
local filesystem_utils = require("utils.filesystem")
2✔
46
local version_utils = require("utils.version")
2✔
47
local update_module = require("modules.update")
2✔
48
local run_module = require("modules.run")
2✔
49
local list_module = require("modules.list")
2✔
50
local self_module = require("modules.self")
2✔
51
local printer = require("utils.printer")
2✔
52

53
local function get_help_string()
54
  local version = version_utils.get_version and version_utils.get_version() or "(unknown)"
×
NEW
55
  return (([[
×
56
Almandine CLI v%s
57

58
Usage: almd [command] [options]
59
     almd [ -h | --help | -v | --version ]
60

61
Project Management:
62
 init                  Initialize a new Lua project in the current directory
63

64
Dependency Management:
65
 add                   Add a dependency to the project
66
 install               Install all dependencies listed in project.lua (aliases: i)
67
 remove                Remove a dependency from the project (aliases: rm, uninstall, un)
68
 update                Update dependencies to latest allowed version (aliases: up)
69
 list                  List installed dependencies and their versions (aliases: ls)
70

71
Scripts:
72
 run                   Run a script defined in project.lua scripts table
73

74
Self-management:
75
 self uninstall        Remove the almd CLI
76
 self update           Update the almd CLI
77

78
Options:
79
-h, --help             Show this help message
80
-v, --version          Show version
81

82
For help with a command: almd help <command> or almd <command> --help
83
]]):format(version))
×
84
end
85

86
local function run_cli(args)
87
  ---
88
  -- Executes the appropriate Almandine command based on arguments.
89
  -- @return boolean success True if command executed without fatal error.
90
  -- @return string|nil output_message Message for stdout (on success or non-fatal error).
91
  -- @return string|nil error_message Error message for stderr (on failure).
92
  ---
93

94
  -- Helper function to get manifest, memoized per run_cli call
95
  local manifest_cache = nil
5✔
96
  local manifest_load_error = nil
97
  local function get_cached_manifest()
98
    if manifest_cache == nil and manifest_load_error == nil then
5✔
99
      manifest_cache, manifest_load_error = manifest_utils.safe_load_project_manifest("project.lua")
5✔
100
    end
101
    return manifest_cache, manifest_load_error
5✔
102
  end
103

104
  version_utils.check_lua_version(get_cached_manifest) -- Still check version early
5✔
105

106
  -- Handle no args or help flags
107
  if not args[1] or args[1] == "--help" or args[1] == "help" or (args[1] and args[1]:match("^%-h")) then
5✔
NEW
108
    return true, get_help_string() -- Return help string directly
×
109
  end
110

111
  -- Handle version flag
112
  if args[1] == "-v" or args[1] == "--version" then
5✔
NEW
113
    local version = version_utils.get_version and version_utils.get_version() or "(unknown)"
×
NEW
114
    return true, "Almandine CLI v" .. version
×
115
  end
116

117
  -- Handle specific command help
118
  if args[1] == "help" or (args[2] and args[2] == "--help") then
5✔
NEW
119
    local cmd = args[2] or args[1] -- Command is the second arg if 'help' is first
×
NEW
120
    if args[1] == "help" and not args[2] then -- `almd help` case
×
NEW
121
      return true, get_help_string() -- Return help string
×
122
    end
123

124
    local help_map = {
×
125
      init = init_module.help_info,
126
      add = add_module.help_info,
127
      install = install_module.help_info,
128
      remove = remove_module.help_info,
129
      update = update_module.help_info,
130
      run = run_module.help_info,
131
      list = list_module.help_info,
132
      ["self"] = self_module.help_info,
133
    }
NEW
134
    if help_map[cmd] then
×
135
      -- Module help functions now RETURN the string
NEW
136
      local help_text = help_map[cmd]()
×
NEW
137
      return true, help_text
×
138
    else
NEW
139
      return false, nil, "Unknown command for help: " .. tostring(cmd)
×
140
    end
141
  end
142

143
  -- --- Command Execution ---
144
  local command = args[1]
5✔
145
  local printer_dep = { printer = printer } -- Create printer dependency table
5✔
146

147
  if command == "init" then
5✔
148
    -- Create dependencies table for init_project
NEW
149
    local init_deps = {
×
150
      prompt = function(prompt_text, default) -- Adjusted prompt wrapper
NEW
151
        io.write(prompt_text)
×
NEW
152
        local input = io.read()
×
NEW
153
        if input == "" or input == nil then
×
NEW
154
          return default
×
155
        else
NEW
156
          return input
×
157
        end
158
      end,
159
      printer = printer, -- Inject the printer
160
      save_manifest = manifest_utils.save_manifest, -- Use the required utility
161
    }
162
    -- Pass dependencies to init_project
NEW
163
    local ok, msg, err = init_module.init_project(init_deps)
×
NEW
164
    return ok, msg, err -- Directly return what init_project gives
×
165
  elseif command == "add" then
5✔
166
    local source = args[2]
5✔
167
    if not source then
5✔
NEW
168
      return false, nil, "Usage: almd add <source> [-d <dir>] [-n <dep_name>] [--verbose]"
×
169
    end
170

171
    local verbose = false
5✔
172
    local dest_dir, dep_name
173
    local i = 3
5✔
174
    while i <= #args do
8✔
175
      if args[i] == "-d" and args[i + 1] then
3✔
176
        dest_dir = args[i + 1]
2✔
177
        i = i + 2
2✔
178
      elseif args[i] == "-n" and args[i + 1] then
1✔
179
        dep_name = args[i + 1]
1✔
180
        i = i + 2
1✔
181
      elseif args[i] == "--verbose" then
×
182
        verbose = true
×
183
        i = i + 1
×
184
      else
NEW
185
        return false, nil, "Unknown or incomplete flag: " .. tostring(args[i])
×
186
      end
187
    end
188

189
    local ok, fatal_err, warning_occurred, warning_msg = add_module.add_dependency(dep_name, source, dest_dir, {
10✔
190
      load_manifest = manifest_utils.safe_load_project_manifest,
5✔
191
      save_manifest = manifest_utils.save_manifest,
5✔
192
      ensure_lib_dir = filesystem_utils.ensure_lib_dir,
5✔
193
      downloader = downloader,
5✔
194
      hash_utils = require("utils.hash"),
5✔
195
      lockfile = require("utils.lockfile"),
5✔
196
      verbose = verbose,
5✔
197
      printer = printer, -- Inject printer
5✔
198
    })
199

200
    local final_message = ""
5✔
201

202
    if warning_occurred then
5✔
203
      -- Warnings go to stdout for now, could be stderr if preferred
204
      final_message = "Warning(s): " .. (warning_msg or "Unknown warning") .. "\n"
1✔
205
    end
206

207
    if not ok then
5✔
208
      local error_message = "Error: Add operation failed."
1✔
209
      if fatal_err then
1✔
210
        error_message = error_message .. "\n  Reason: " .. fatal_err
1✔
211
      end
212
      -- Return warning message on stdout even if error occurred
213
      return false, final_message:gsub("\n$", ""), error_message
1✔
214
    else
215
      -- Combine warnings and success message
216
      final_message = final_message .. "Dependency added successfully."
4✔
217
      return true, final_message
4✔
218
    end
219

NEW
220
  elseif command == "install" or command == "i" then
×
221
    local dep_name = args[2]
×
222
    local deps = {
×
223
      load_manifest = get_cached_manifest,
224
      ensure_lib_dir = filesystem_utils.ensure_lib_dir,
225
      downloader = downloader,
226
      lockfile = require("utils.lockfile"),
227
      hash_utils = require("utils.hash"),
228
      filesystem = filesystem_utils,
229
      url_utils = require("utils.url"),
230
      printer = printer, -- Inject printer
231
    }
NEW
232
    local ok, msg = install_module.install_dependencies(dep_name, deps)
×
233
    if not ok then
×
NEW
234
      return false, nil, "Installation failed: " .. tostring(msg or "Unknown error")
×
235
    end
NEW
236
    return true, msg or "Installation complete."
×
237

NEW
238
  elseif command == "remove" or command == "rm" or command == "uninstall" or command == "un" then
×
NEW
239
    local dep_name = args[2]
×
NEW
240
    if not dep_name then
×
NEW
241
      return false, nil, "Usage: almd remove <dep_name>"
×
242
    end
NEW
243
    local ok, msg, err = remove_module.remove_dependency(
×
244
      dep_name,
245
      get_cached_manifest,
NEW
246
      manifest_utils.save_manifest,
×
247
      printer_dep -- Pass printer dependency
248
    )
NEW
249
    return ok, msg, err
×
250

NEW
251
  elseif command == "update" or command == "up" then
×
252
    local latest = false
×
253
    for i = 2, #args do
×
254
      if args[i] == "--latest" then
×
255
        latest = true
×
256
      end
257
    end
258
    update_module.update_dependencies(
×
259
      get_cached_manifest,
260
      manifest_utils.save_manifest,
×
261
      filesystem_utils.ensure_lib_dir,
×
262
      {
263
        downloader = downloader,
264
        printer = printer, -- Inject printer
265
      },
266
      add_module.resolve_latest_version,
×
267
      latest
268
    )
269
    -- Update likely prints its own progress, return a simple status
NEW
270
    return true, "Update process finished. Check output for details."
×
271

NEW
272
  elseif command == "run" then
×
NEW
273
    local script_name = args[2]
×
NEW
274
    if not script_name then
×
NEW
275
      return false, nil, "Usage: almd run <script_name>"
×
276
    end
NEW
277
    local deps = { manifest_loader = get_cached_manifest, printer = printer }
×
NEW
278
    local ok, msg, err = run_module.run_script(script_name, deps)
×
NEW
279
    return ok, msg, err -- run_script should return msg for stdout, err for stderr
×
280

NEW
281
  elseif command == "list" or command == "ls" then
×
282
    -- list_dependencies will now return the string
NEW
283
    local ok, list_str, err = list_module.list_dependencies(get_cached_manifest, printer_dep)
×
NEW
284
    return ok, list_str, err
×
285

NEW
286
  elseif command == "self" and args[2] == "uninstall" then
×
NEW
287
    local ok, err = self_module.uninstall_self(printer_dep)
×
288
    if ok then
×
NEW
289
      return true, "almd self uninstall: Success."
×
290
    else
NEW
291
      return false, nil, "almd self uninstall: Failed.\n" .. (err or "Unknown error.")
×
292
    end
NEW
293
  elseif command == "self" and args[2] == "update" then
×
NEW
294
    local ok, msg, err = self_module.self_update(printer_dep)
×
295
    if ok then
×
NEW
296
      return true, msg or "almd self update: Success."
×
NEW
297
    elseif msg == "Update staged for next run." then
×
NEW
298
      return true, msg -- Special case, considered success
×
299
    else
NEW
300
      return false, msg, "almd self update: Failed.\n" .. (err or "Unknown error.")
×
301
    end
302

NEW
303
  elseif not run_module.is_reserved_command(command) then
×
304
    -- Check for unambiguous script name if not a reserved command
NEW
305
    local deps = { manifest_loader = get_cached_manifest, printer = printer }
×
NEW
306
    local script_name = run_module.get_unambiguous_script(command, deps)
×
307
    if script_name then
×
NEW
308
      local ok, msg, err = run_module.run_script(script_name, deps)
×
NEW
309
      return ok, msg, err
×
310
    end
311
  end
312

313
  -- If command wasn't handled, return generic help/error on stderr
NEW
314
  return false, nil, "Unknown command or usage error: '" .. tostring(command) .. "'\n\n" .. get_help_string()
×
315
end
316

317
-- Wrapper for run_cli to handle exit code and printing
318
local function main(...)
NEW
319
  local ok, output_message, error_message = run_cli({ ... })
×
320

NEW
321
  if output_message then
×
NEW
322
    printer.stdout(output_message) -- Print regular output to stdout
×
323
  end
324

NEW
325
  if error_message then
×
NEW
326
    printer.stderr(error_message) -- Print errors to stderr
×
327
  end
328

NEW
329
  if not ok then
×
NEW
330
    os.exit(1)
×
331
  else
NEW
332
    os.exit(0) -- Ensure explicit exit 0 on success
×
333
  end
334
end
335

336
-- Get the script's own path using debug.getinfo
337
local script_path = debug.getinfo(1, "S").source:sub(2) -- Remove leading '@'
2✔
338
script_path = script_path:gsub("\\", "/") -- Normalize separators
2✔
339

340
-- Entry point check: Compare arg[0] with the script's path
341
if arg and arg[0] and script_path:match(arg[0] .. "$") then
2✔
NEW
342
  main(unpack(arg or {}, 1)) -- Use arg or empty table, unpack from index 1
×
343
end
344

345
-- Export the run_cli function for testing or programmatic use
346
return {
2✔
347
  run_cli = run_cli,
2✔
348
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc