@tolinax/ayoune-cli 2026.3.0 → 2026.4.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/data/contextSlots.js +189 -0
- package/data/modelsAndRights.js +56 -0
- package/data/modules.js +16 -0
- package/lib/api/apiCallHandler.js +6 -2
- package/lib/api/apiClient.js +9 -1
- package/lib/api/auditCallHandler.js +2 -2
- package/lib/api/handleAPIError.js +20 -18
- package/lib/api/login.js +3 -3
- package/lib/api/searchClient.js +119 -0
- package/lib/commands/createAccessCommand.js +126 -0
- package/lib/commands/createActionsCommand.js +40 -9
- package/lib/commands/createAiCommand.js +17 -17
- package/lib/commands/createAliasCommand.js +4 -6
- package/lib/commands/createAuditCommand.js +5 -9
- package/lib/commands/createBatchCommand.js +15 -28
- package/lib/commands/createCompletionsCommand.js +6 -3
- package/lib/commands/createConfigCommand.js +8 -14
- package/lib/commands/createContextCommand.js +163 -0
- package/lib/commands/createCopyCommand.js +4 -7
- package/lib/commands/createCreateCommand.js +4 -7
- package/lib/commands/createDeleteCommand.js +4 -6
- package/lib/commands/createDeployCommand.js +31 -55
- package/lib/commands/createDescribeCommand.js +12 -10
- package/lib/commands/createEditCommand.js +13 -8
- package/lib/commands/createEventsCommand.js +4 -4
- package/lib/commands/createExecCommand.js +65 -35
- package/lib/commands/createExportCommand.js +21 -24
- package/lib/commands/createGetCommand.js +13 -14
- package/lib/commands/createJobsCommand.js +8 -13
- package/lib/commands/createListCommand.js +13 -14
- package/lib/commands/createLoginCommand.js +16 -4
- package/lib/commands/createLogoutCommand.js +2 -2
- package/lib/commands/createModulesCommand.js +16 -19
- package/lib/commands/createMonitorCommand.js +9 -16
- package/lib/commands/createPermissionsCommand.js +10 -18
- package/lib/commands/createProgram.js +47 -21
- package/lib/commands/createSearchCommand.js +219 -69
- package/lib/commands/createSelfHostUpdateCommand.js +166 -0
- package/lib/commands/createServicesCommand.js +5 -8
- package/lib/commands/createSetupCommand.js +305 -0
- package/lib/commands/createStatusCommand.js +147 -0
- package/lib/commands/createStorageCommand.js +2 -3
- package/lib/commands/createStreamCommand.js +4 -4
- package/lib/commands/createSyncCommand.js +5 -8
- package/lib/commands/createTemplateCommand.js +9 -16
- package/lib/commands/createUpdateCommand.js +12 -15
- package/lib/commands/createUsersCommand.js +21 -31
- package/lib/commands/createWebhooksCommand.js +15 -22
- package/lib/commands/createWhoAmICommand.js +8 -6
- package/lib/helpers/cliError.js +24 -0
- package/lib/helpers/config.js +1 -0
- package/lib/helpers/configLoader.js +6 -0
- package/lib/helpers/contextInjector.js +65 -0
- package/lib/helpers/contextResolver.js +70 -0
- package/lib/helpers/contextStore.js +46 -0
- package/lib/helpers/handleResponseFormatOptions.js +59 -10
- package/lib/helpers/logo.js +48 -0
- package/lib/helpers/resolveCollectionArgs.js +36 -0
- package/lib/helpers/sanitizeFields.js +18 -0
- package/lib/helpers/secureStorage.js +72 -0
- package/lib/helpers/tokenPayload.js +21 -0
- package/lib/helpers/updateNotifier.js +49 -0
- package/lib/models/getModuleFromCollection.js +4 -1
- package/lib/operations/handleCopySingleOperation.js +10 -2
- package/lib/operations/handleCreateSingleOperation.js +3 -0
- package/lib/operations/handleDescribeSingleOperation.js +23 -0
- package/lib/operations/handleGetOperation.js +9 -3
- package/lib/operations/handleListOperation.js +14 -10
- package/lib/prompts/promptModule.js +9 -6
- package/package.json +163 -158
|
@@ -1,101 +1,251 @@
|
|
|
1
|
+
import { resolveCollectionArgs } from "../helpers/resolveCollectionArgs.js";
|
|
1
2
|
import { getModuleFromCollection } from "../models/getModuleFromCollection.js";
|
|
2
3
|
import { apiCallHandler } from "../api/apiCallHandler.js";
|
|
4
|
+
import { searchModel, searchOne, searchGlobal } from "../api/searchClient.js";
|
|
3
5
|
import { handleResponseFormatOptions } from "../helpers/handleResponseFormatOptions.js";
|
|
4
6
|
import { saveFile } from "../helpers/saveFile.js";
|
|
5
7
|
import { localStorage } from "../helpers/localStorage.js";
|
|
6
8
|
import { spinner } from "../../index.js";
|
|
7
9
|
import { EXIT_GENERAL_ERROR } from "../exitCodes.js";
|
|
10
|
+
import { cliError } from "../helpers/cliError.js";
|
|
11
|
+
import { sanitizeFields } from "../helpers/sanitizeFields.js";
|
|
12
|
+
import { getContextFilterParams } from "../helpers/contextInjector.js";
|
|
8
13
|
export function createSearchCommand(program) {
|
|
9
14
|
program
|
|
10
|
-
.command("search
|
|
15
|
+
.command("search [collectionOrModule] [collectionOrQuery] [query]")
|
|
11
16
|
.alias("find")
|
|
12
|
-
.description("Search entries
|
|
17
|
+
.description("Search entries via the aYOUne Search Service")
|
|
13
18
|
.addHelpText("after", `
|
|
14
19
|
Examples:
|
|
15
|
-
ay search
|
|
20
|
+
ay search consumers "John" Search via search service
|
|
21
|
+
ay search crm consumers "John" Search with explicit module
|
|
22
|
+
ay search -g "John" Global search across all collections (SSE)
|
|
23
|
+
ay search consumers "John" --one Find first match
|
|
24
|
+
ay search consumers "John" --field name Search specific field
|
|
16
25
|
ay search contacts --filter "status=active" Filter by field
|
|
17
|
-
ay search products --filter "price>100" Comparison filter
|
|
18
26
|
ay search orders --filter "status=paid" --sort "-createdAt"
|
|
19
|
-
ay search
|
|
27
|
+
ay search consumers "John" --legacy Use legacy module API
|
|
20
28
|
ay find invoices --filter "total>500" --count Just count matches`)
|
|
29
|
+
.option("-g, --global <query>", "Global search across all collections (SSE streaming)")
|
|
30
|
+
.option("--field <name>", "Search in a specific field")
|
|
31
|
+
.option("--one", "Return only the first matching entry", false)
|
|
32
|
+
.option("--legacy", "Use legacy module API instead of search service", false)
|
|
21
33
|
.option("--filter <filters>", "Comma-separated key=value filters (supports =, !=, >, <, >=, <=)")
|
|
22
34
|
.option("--fields <fields>", "Comma-separated fields to return (projection)")
|
|
23
35
|
.option("--sort <field>", "Sort by field (prefix with - for descending)", "-createdAt")
|
|
24
36
|
.option("--count", "Only return count of matching entries", false)
|
|
25
37
|
.option("-l, --limit <number>", "Limit results", parseInt, 25)
|
|
26
38
|
.option("-p, --page <number>", "Page number", parseInt, 1)
|
|
27
|
-
.action(async (
|
|
28
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
39
|
+
.action(async (collectionOrModule, collectionOrQuery, query, options) => {
|
|
29
40
|
try {
|
|
30
41
|
const opts = { ...program.opts(), ...options };
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
page: opts.page,
|
|
35
|
-
limit: opts.limit,
|
|
36
|
-
sort: opts.sort,
|
|
37
|
-
responseFormat: opts.responseFormat,
|
|
38
|
-
verbosity: opts.verbosity,
|
|
39
|
-
hideMeta: opts.hideMeta,
|
|
40
|
-
};
|
|
41
|
-
if (query) {
|
|
42
|
-
params.q = query;
|
|
42
|
+
// Mode 1: Global SSE search
|
|
43
|
+
if (opts.global) {
|
|
44
|
+
return await handleGlobalSearch(opts);
|
|
43
45
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
// For all other modes, we need a collection
|
|
47
|
+
if (!collectionOrModule) {
|
|
48
|
+
cliError("Collection name is required (or use --global for global search)", EXIT_GENERAL_ERROR);
|
|
49
|
+
return;
|
|
46
50
|
}
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const [, key, op, value] = match;
|
|
54
|
-
if (op === "=") {
|
|
55
|
-
params[key] = value;
|
|
56
|
-
}
|
|
57
|
-
else if (op === "!=") {
|
|
58
|
-
params[`${key}[ne]`] = value;
|
|
59
|
-
}
|
|
60
|
-
else if (op === ">") {
|
|
61
|
-
params[`${key}[gt]`] = value;
|
|
62
|
-
}
|
|
63
|
-
else if (op === "<") {
|
|
64
|
-
params[`${key}[lt]`] = value;
|
|
65
|
-
}
|
|
66
|
-
else if (op === ">=") {
|
|
67
|
-
params[`${key}[gte]`] = value;
|
|
68
|
-
}
|
|
69
|
-
else if (op === "<=") {
|
|
70
|
-
params[`${key}[lte]`] = value;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
51
|
+
// Detect if first arg is a module: ay search crm consumers "John"
|
|
52
|
+
let resolved;
|
|
53
|
+
let searchQuery = query;
|
|
54
|
+
const m = getModuleFromCollection(collectionOrModule.toLowerCase());
|
|
55
|
+
if (!m && collectionOrQuery) {
|
|
56
|
+
resolved = resolveCollectionArgs(collectionOrModule, collectionOrQuery);
|
|
74
57
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
spinner.success({ text: `${total} matching entries in ${collection}` });
|
|
79
|
-
spinner.stop();
|
|
80
|
-
console.log(total);
|
|
81
|
-
return;
|
|
58
|
+
else {
|
|
59
|
+
resolved = resolveCollectionArgs(collectionOrModule);
|
|
60
|
+
searchQuery = collectionOrQuery;
|
|
82
61
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
await saveFile("search", opts, res);
|
|
62
|
+
const collection = resolved.collection;
|
|
63
|
+
// Mode 4: Legacy module API search
|
|
64
|
+
if (opts.legacy) {
|
|
65
|
+
return await handleLegacySearch(resolved, searchQuery, opts);
|
|
66
|
+
}
|
|
67
|
+
// Mode 3: FindOne via search service
|
|
68
|
+
if (opts.one) {
|
|
69
|
+
return await handleFindOneSearch(resolved, searchQuery, opts);
|
|
70
|
+
}
|
|
71
|
+
// Mode 2: Single-model search via search service
|
|
72
|
+
return await handleModelSearch(resolved, searchQuery, opts);
|
|
95
73
|
}
|
|
96
74
|
catch (e) {
|
|
97
|
-
|
|
98
|
-
process.exit(EXIT_GENERAL_ERROR);
|
|
75
|
+
cliError(e.message || "Search failed", EXIT_GENERAL_ERROR);
|
|
99
76
|
}
|
|
100
77
|
});
|
|
101
78
|
}
|
|
79
|
+
async function handleGlobalSearch(opts) {
|
|
80
|
+
const query = opts.global;
|
|
81
|
+
spinner.start({ text: `Global search for "${query}"...`, color: "magenta" });
|
|
82
|
+
const results = await searchGlobal(query, opts.limit, {
|
|
83
|
+
onResult: (result) => {
|
|
84
|
+
spinner.update({
|
|
85
|
+
text: `Searching... found ${result.total} in ${result.collection}`,
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
onError: (error) => {
|
|
89
|
+
spinner.update({ text: `Search warning: ${error}` });
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
// Flatten results into a grouped response for handleResponseFormatOptions
|
|
93
|
+
const totalEntries = results.reduce((sum, r) => sum + r.total, 0);
|
|
94
|
+
const allEntries = results.flatMap((r) => r.entries.map((e) => ({ ...e, _collection: r.collection })));
|
|
95
|
+
const res = {
|
|
96
|
+
payload: allEntries,
|
|
97
|
+
meta: {
|
|
98
|
+
responseTime: 0,
|
|
99
|
+
pageInfo: { totalEntries: totalEntries, page: 1, totalPages: 1 },
|
|
100
|
+
collections: results.map((r) => ({ name: r.collection, count: r.total })),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
if (opts.count) {
|
|
104
|
+
spinner.success({ text: `${totalEntries} matching entries across ${results.length} collections` });
|
|
105
|
+
spinner.stop();
|
|
106
|
+
console.log(totalEntries);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
handleResponseFormatOptions(opts, res);
|
|
110
|
+
spinner.success({
|
|
111
|
+
text: `Found ${totalEntries} entries across ${results.length} collections`,
|
|
112
|
+
});
|
|
113
|
+
spinner.stop();
|
|
114
|
+
if (opts.save)
|
|
115
|
+
await saveFile("search-global", opts, res);
|
|
116
|
+
}
|
|
117
|
+
async function handleModelSearch(resolved, searchQuery, opts) {
|
|
118
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
119
|
+
const collection = resolved.collection;
|
|
120
|
+
spinner.start({ text: `Searching ${collection}...`, color: "magenta" });
|
|
121
|
+
const params = {
|
|
122
|
+
limit: opts.limit,
|
|
123
|
+
skip: (opts.page - 1) * opts.limit,
|
|
124
|
+
};
|
|
125
|
+
if (searchQuery)
|
|
126
|
+
params.q = searchQuery;
|
|
127
|
+
if (opts.field)
|
|
128
|
+
params.field = opts.field;
|
|
129
|
+
if (opts.fields)
|
|
130
|
+
params.fields = sanitizeFields(opts.fields).join(",");
|
|
131
|
+
if (opts.sort)
|
|
132
|
+
params.sort = opts.sort;
|
|
133
|
+
// Inject context params
|
|
134
|
+
const contextParams = getContextFilterParams(collection);
|
|
135
|
+
Object.assign(params, contextParams);
|
|
136
|
+
// Parse filters
|
|
137
|
+
applyFilters(params, opts.filter);
|
|
138
|
+
const res = await searchModel(collection, params);
|
|
139
|
+
if (opts.count) {
|
|
140
|
+
const total = (_c = (_b = (_a = res.meta) === null || _a === void 0 ? void 0 : _a.pageInfo) === null || _b === void 0 ? void 0 : _b.totalEntries) !== null && _c !== void 0 ? _c : 0;
|
|
141
|
+
spinner.success({ text: `${total} matching entries in ${collection}` });
|
|
142
|
+
spinner.stop();
|
|
143
|
+
console.log(total);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
handleResponseFormatOptions(opts, res);
|
|
147
|
+
const total = (_f = (_e = (_d = res.meta) === null || _d === void 0 ? void 0 : _d.pageInfo) === null || _e === void 0 ? void 0 : _e.totalEntries) !== null && _f !== void 0 ? _f : 0;
|
|
148
|
+
const page = (_j = (_h = (_g = res.meta) === null || _g === void 0 ? void 0 : _g.pageInfo) === null || _h === void 0 ? void 0 : _h.page) !== null && _j !== void 0 ? _j : 1;
|
|
149
|
+
const totalPages = (_m = (_l = (_k = res.meta) === null || _k === void 0 ? void 0 : _k.pageInfo) === null || _l === void 0 ? void 0 : _l.totalPages) !== null && _m !== void 0 ? _m : 1;
|
|
150
|
+
spinner.success({
|
|
151
|
+
text: `Found ${total} entries in ${collection} (page ${page}/${totalPages})`,
|
|
152
|
+
});
|
|
153
|
+
spinner.stop();
|
|
154
|
+
localStorage.setItem("lastModule", resolved.module);
|
|
155
|
+
localStorage.setItem("lastCollection", resolved.collection);
|
|
156
|
+
if (opts.save)
|
|
157
|
+
await saveFile("search", opts, res);
|
|
158
|
+
}
|
|
159
|
+
async function handleFindOneSearch(resolved, searchQuery, opts) {
|
|
160
|
+
const collection = resolved.collection;
|
|
161
|
+
spinner.start({ text: `Finding one in ${collection}...`, color: "magenta" });
|
|
162
|
+
const params = {};
|
|
163
|
+
if (searchQuery)
|
|
164
|
+
params.q = searchQuery;
|
|
165
|
+
if (opts.field)
|
|
166
|
+
params.field = opts.field;
|
|
167
|
+
if (opts.fields)
|
|
168
|
+
params.fields = sanitizeFields(opts.fields).join(",");
|
|
169
|
+
const contextParams = getContextFilterParams(collection);
|
|
170
|
+
Object.assign(params, contextParams);
|
|
171
|
+
applyFilters(params, opts.filter);
|
|
172
|
+
const res = await searchOne(collection, params);
|
|
173
|
+
handleResponseFormatOptions(opts, res);
|
|
174
|
+
spinner.success({
|
|
175
|
+
text: `Found match in ${collection}`,
|
|
176
|
+
});
|
|
177
|
+
spinner.stop();
|
|
178
|
+
localStorage.setItem("lastModule", resolved.module);
|
|
179
|
+
localStorage.setItem("lastCollection", resolved.collection);
|
|
180
|
+
if (opts.save)
|
|
181
|
+
await saveFile("search-one", opts, res);
|
|
182
|
+
}
|
|
183
|
+
async function handleLegacySearch(resolved, searchQuery, opts) {
|
|
184
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
185
|
+
const collection = resolved.collection;
|
|
186
|
+
spinner.start({ text: `Searching ${collection} (legacy)...`, color: "magenta" });
|
|
187
|
+
const params = {
|
|
188
|
+
page: opts.page,
|
|
189
|
+
limit: opts.limit,
|
|
190
|
+
sort: opts.sort,
|
|
191
|
+
responseFormat: opts.responseFormat,
|
|
192
|
+
verbosity: opts.verbosity,
|
|
193
|
+
hideMeta: opts.hideMeta,
|
|
194
|
+
};
|
|
195
|
+
const contextParams = getContextFilterParams(collection);
|
|
196
|
+
Object.assign(params, contextParams);
|
|
197
|
+
if (searchQuery)
|
|
198
|
+
params.q = searchQuery;
|
|
199
|
+
if (opts.fields)
|
|
200
|
+
params.fields = sanitizeFields(opts.fields).join(",");
|
|
201
|
+
applyFilters(params, opts.filter);
|
|
202
|
+
const res = await apiCallHandler(resolved.module, collection, "get", null, params);
|
|
203
|
+
if (opts.count) {
|
|
204
|
+
const total = (_c = (_b = (_a = res.meta) === null || _a === void 0 ? void 0 : _a.pageInfo) === null || _b === void 0 ? void 0 : _b.totalEntries) !== null && _c !== void 0 ? _c : 0;
|
|
205
|
+
spinner.success({ text: `${total} matching entries in ${collection}` });
|
|
206
|
+
spinner.stop();
|
|
207
|
+
console.log(total);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
handleResponseFormatOptions(opts, res);
|
|
211
|
+
const total = (_f = (_e = (_d = res.meta) === null || _d === void 0 ? void 0 : _d.pageInfo) === null || _e === void 0 ? void 0 : _e.totalEntries) !== null && _f !== void 0 ? _f : 0;
|
|
212
|
+
const page = (_j = (_h = (_g = res.meta) === null || _g === void 0 ? void 0 : _g.pageInfo) === null || _h === void 0 ? void 0 : _h.page) !== null && _j !== void 0 ? _j : 1;
|
|
213
|
+
const totalPages = (_m = (_l = (_k = res.meta) === null || _k === void 0 ? void 0 : _k.pageInfo) === null || _l === void 0 ? void 0 : _l.totalPages) !== null && _m !== void 0 ? _m : 1;
|
|
214
|
+
spinner.success({
|
|
215
|
+
text: `Found ${total} entries in ${collection} (page ${page}/${totalPages})`,
|
|
216
|
+
});
|
|
217
|
+
spinner.stop();
|
|
218
|
+
localStorage.setItem("lastModule", resolved.module);
|
|
219
|
+
localStorage.setItem("lastCollection", resolved.collection);
|
|
220
|
+
if (opts.save)
|
|
221
|
+
await saveFile("search", opts, res);
|
|
222
|
+
}
|
|
223
|
+
function applyFilters(params, filter) {
|
|
224
|
+
if (!filter)
|
|
225
|
+
return;
|
|
226
|
+
const filters = filter.split(",");
|
|
227
|
+
for (const f of filters) {
|
|
228
|
+
const match = f.match(/^(\w+)(!=|>=|<=|>|<|=)(.+)$/);
|
|
229
|
+
if (match) {
|
|
230
|
+
const [, key, op, value] = match;
|
|
231
|
+
if (op === "=") {
|
|
232
|
+
params[key] = value;
|
|
233
|
+
}
|
|
234
|
+
else if (op === "!=") {
|
|
235
|
+
params[`${key}[ne]`] = value;
|
|
236
|
+
}
|
|
237
|
+
else if (op === ">") {
|
|
238
|
+
params[`${key}[gt]`] = value;
|
|
239
|
+
}
|
|
240
|
+
else if (op === "<") {
|
|
241
|
+
params[`${key}[lt]`] = value;
|
|
242
|
+
}
|
|
243
|
+
else if (op === ">=") {
|
|
244
|
+
params[`${key}[gte]`] = value;
|
|
245
|
+
}
|
|
246
|
+
else if (op === "<=") {
|
|
247
|
+
params[`${key}[lte]`] = value;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { spinner } from "../../index.js";
|
|
3
|
+
import { EXIT_GENERAL_ERROR } from "../exitCodes.js";
|
|
4
|
+
import { cliError } from "../helpers/cliError.js";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
function runCommand(cmd) {
|
|
7
|
+
try {
|
|
8
|
+
return execSync(cmd, { encoding: "utf-8", timeout: 30000 }).trim();
|
|
9
|
+
}
|
|
10
|
+
catch (_a) {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function detectRuntime() {
|
|
15
|
+
// Check for docker compose
|
|
16
|
+
const composeResult = runCommand("docker compose version 2>&1");
|
|
17
|
+
if (composeResult.includes("Docker Compose"))
|
|
18
|
+
return "compose";
|
|
19
|
+
// Check for kubectl
|
|
20
|
+
const kubectlResult = runCommand("kubectl version --client 2>&1");
|
|
21
|
+
if (kubectlResult.includes("Client Version"))
|
|
22
|
+
return "kubernetes";
|
|
23
|
+
return "unknown";
|
|
24
|
+
}
|
|
25
|
+
function getRunningComposeServices() {
|
|
26
|
+
const output = runCommand('docker compose ps --format "{{.Name}}" 2>&1');
|
|
27
|
+
if (!output)
|
|
28
|
+
return [];
|
|
29
|
+
return output.split("\n").filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
function getComposeImageVersions() {
|
|
32
|
+
const versions = new Map();
|
|
33
|
+
const output = runCommand('docker compose ps --format "{{.Name}}|{{.Image}}" 2>&1');
|
|
34
|
+
if (!output)
|
|
35
|
+
return versions;
|
|
36
|
+
for (const line of output.split("\n")) {
|
|
37
|
+
const [name, image] = line.split("|");
|
|
38
|
+
if (name && image) {
|
|
39
|
+
const tag = image.split(":").pop() || "unknown";
|
|
40
|
+
versions.set(name, tag);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return versions;
|
|
44
|
+
}
|
|
45
|
+
export function createSelfHostUpdateCommand(program) {
|
|
46
|
+
program
|
|
47
|
+
.command("self-host-update")
|
|
48
|
+
.alias("shu")
|
|
49
|
+
.description("Check for and apply updates to a self-hosted aYOUne deployment")
|
|
50
|
+
.addHelpText("after", `
|
|
51
|
+
Examples:
|
|
52
|
+
ay self-host-update Check for available updates
|
|
53
|
+
ay self-host-update --apply Pull new images and restart services
|
|
54
|
+
ay self-host-update --service crm Check updates for a specific service`)
|
|
55
|
+
.option("--apply", "Apply available updates (pull new images, restart services)")
|
|
56
|
+
.option("--service <name>", "Update only a specific service")
|
|
57
|
+
.action(async (options) => {
|
|
58
|
+
try {
|
|
59
|
+
const opts = { ...program.opts(), ...options };
|
|
60
|
+
const runtime = detectRuntime();
|
|
61
|
+
if (runtime === "unknown") {
|
|
62
|
+
cliError("Could not detect Docker Compose or Kubernetes. Ensure docker or kubectl is installed and accessible.", EXIT_GENERAL_ERROR);
|
|
63
|
+
}
|
|
64
|
+
console.log(chalk.cyan.bold("\n aYOUne Self-Hosted Update\n"));
|
|
65
|
+
console.log(chalk.dim(` Runtime: ${runtime === "compose" ? "Docker Compose" : "Kubernetes"}\n`));
|
|
66
|
+
if (runtime === "compose") {
|
|
67
|
+
await handleComposeUpdate(opts);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
await handleKubernetesUpdate(opts);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
cliError(e.message || "Update check failed", EXIT_GENERAL_ERROR);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function handleComposeUpdate(opts) {
|
|
79
|
+
const services = getRunningComposeServices();
|
|
80
|
+
if (services.length === 0) {
|
|
81
|
+
console.log(chalk.yellow(" No running aYOUne services found."));
|
|
82
|
+
console.log(chalk.dim(" Start services first: docker compose --profile core up -d\n"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const versions = getComposeImageVersions();
|
|
86
|
+
console.log(chalk.white(` Running services: ${services.length}\n`));
|
|
87
|
+
for (const [name, version] of versions) {
|
|
88
|
+
if (opts.service && !name.includes(opts.service))
|
|
89
|
+
continue;
|
|
90
|
+
console.log(` ${chalk.dim("●")} ${name} ${chalk.dim(`(${version})`)}`);
|
|
91
|
+
}
|
|
92
|
+
console.log("");
|
|
93
|
+
if (opts.apply) {
|
|
94
|
+
spinner.start({ text: "Pulling latest images...", color: "cyan" });
|
|
95
|
+
const pullTarget = opts.service ? opts.service : "";
|
|
96
|
+
const pullResult = runCommand(`docker compose pull ${pullTarget} 2>&1`);
|
|
97
|
+
if (!pullResult) {
|
|
98
|
+
spinner.error({ text: "Failed to pull images" });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
spinner.success({ text: "Images pulled successfully" });
|
|
102
|
+
spinner.start({ text: "Restarting services...", color: "cyan" });
|
|
103
|
+
const upTarget = opts.service ? opts.service : "";
|
|
104
|
+
runCommand(`docker compose up -d ${upTarget} 2>&1`);
|
|
105
|
+
spinner.success({ text: "Services restarted" });
|
|
106
|
+
spinner.stop();
|
|
107
|
+
console.log(chalk.green("\n Update complete!"));
|
|
108
|
+
console.log(chalk.dim(" Run 'ay status' to verify all services are healthy.\n"));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
spinner.start({ text: "Checking for new images...", color: "cyan" });
|
|
112
|
+
const pullTarget = opts.service ? opts.service : "";
|
|
113
|
+
const dryRunResult = runCommand(`docker compose pull --dry-run ${pullTarget} 2>&1`);
|
|
114
|
+
spinner.stop();
|
|
115
|
+
if (dryRunResult) {
|
|
116
|
+
console.log(chalk.dim(" Image check output:"));
|
|
117
|
+
for (const line of dryRunResult.split("\n")) {
|
|
118
|
+
console.log(chalk.dim(` ${line}`));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.log(chalk.cyan("\n To apply updates, run:"));
|
|
122
|
+
console.log(chalk.dim(" ay self-host-update --apply\n"));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function handleKubernetesUpdate(opts) {
|
|
126
|
+
// Check for Helm releases
|
|
127
|
+
const helmList = runCommand("helm list -n ayoune --output json 2>&1");
|
|
128
|
+
if (!helmList || helmList.startsWith("[")) {
|
|
129
|
+
try {
|
|
130
|
+
const releases = JSON.parse(helmList || "[]");
|
|
131
|
+
if (releases.length === 0) {
|
|
132
|
+
console.log(chalk.yellow(" No Helm releases found in 'ayoune' namespace."));
|
|
133
|
+
console.log(chalk.dim(" Install: helm install ayoune tolinax/ayoune -f values.yaml\n"));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const release of releases) {
|
|
137
|
+
console.log(` ${chalk.white(release.name)} ${chalk.dim(`v${release.app_version} (chart: ${release.chart})`)}`);
|
|
138
|
+
console.log(` ${chalk.dim(`Status: ${release.status} | Updated: ${release.updated}`)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (_a) {
|
|
142
|
+
console.log(chalk.yellow(" Could not parse Helm releases."));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
console.log("");
|
|
146
|
+
if (opts.apply) {
|
|
147
|
+
spinner.start({ text: "Updating Helm repo...", color: "cyan" });
|
|
148
|
+
runCommand("helm repo update 2>&1");
|
|
149
|
+
spinner.success({ text: "Helm repo updated" });
|
|
150
|
+
spinner.start({ text: "Upgrading release...", color: "cyan" });
|
|
151
|
+
const upgradeResult = runCommand("helm upgrade ayoune tolinax/ayoune -n ayoune --reuse-values 2>&1");
|
|
152
|
+
if (upgradeResult.includes("Error")) {
|
|
153
|
+
spinner.error({ text: `Helm upgrade failed: ${upgradeResult}` });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
spinner.success({ text: "Helm release upgraded" });
|
|
157
|
+
spinner.stop();
|
|
158
|
+
console.log(chalk.green("\n Update complete!"));
|
|
159
|
+
console.log(chalk.dim(" Run 'ay status' to verify all services are healthy.\n"));
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.log(chalk.cyan(" To apply updates, run:"));
|
|
163
|
+
console.log(chalk.dim(" ay self-host-update --apply"));
|
|
164
|
+
console.log(chalk.dim(" # or manually: helm upgrade ayoune tolinax/ayoune -n ayoune --reuse-values\n"));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -3,6 +3,7 @@ import { handleResponseFormatOptions } from "../helpers/handleResponseFormatOpti
|
|
|
3
3
|
import { saveFile } from "../helpers/saveFile.js";
|
|
4
4
|
import { spinner } from "../../index.js";
|
|
5
5
|
import { EXIT_GENERAL_ERROR } from "../exitCodes.js";
|
|
6
|
+
import { cliError } from "../helpers/cliError.js";
|
|
6
7
|
import { aYOUneModules } from "../../data/modules.js";
|
|
7
8
|
import { aYOUneServices } from "../../data/services.js";
|
|
8
9
|
function buildServiceRegistry() {
|
|
@@ -61,8 +62,7 @@ export function createServicesCommand(program) {
|
|
|
61
62
|
await saveFile("services-list", opts, res);
|
|
62
63
|
}
|
|
63
64
|
catch (e) {
|
|
64
|
-
|
|
65
|
-
process.exit(EXIT_GENERAL_ERROR);
|
|
65
|
+
cliError(e.message || "Failed to list services", EXIT_GENERAL_ERROR);
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
// ay services endpoints <host>
|
|
@@ -110,8 +110,7 @@ Examples:
|
|
|
110
110
|
await saveFile("service-endpoints", opts, formattedRes);
|
|
111
111
|
}
|
|
112
112
|
catch (e) {
|
|
113
|
-
|
|
114
|
-
process.exit(EXIT_GENERAL_ERROR);
|
|
113
|
+
cliError(e.message || "Failed to fetch endpoints", EXIT_GENERAL_ERROR);
|
|
115
114
|
}
|
|
116
115
|
});
|
|
117
116
|
// ay services health [host]
|
|
@@ -174,8 +173,7 @@ Examples:
|
|
|
174
173
|
await saveFile("services-health", opts, res);
|
|
175
174
|
}
|
|
176
175
|
catch (e) {
|
|
177
|
-
|
|
178
|
-
process.exit(EXIT_GENERAL_ERROR);
|
|
176
|
+
cliError(e.message || "Health check failed", EXIT_GENERAL_ERROR);
|
|
179
177
|
}
|
|
180
178
|
});
|
|
181
179
|
// ay services describe <module>
|
|
@@ -221,8 +219,7 @@ Examples:
|
|
|
221
219
|
spinner.stop();
|
|
222
220
|
}
|
|
223
221
|
catch (e) {
|
|
224
|
-
|
|
225
|
-
process.exit(EXIT_GENERAL_ERROR);
|
|
222
|
+
cliError(e.message || "Failed to describe service", EXIT_GENERAL_ERROR);
|
|
226
223
|
}
|
|
227
224
|
});
|
|
228
225
|
}
|