@hasna/instructions 0.4.19 → 0.4.21
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 +12 -4
- package/dist/cli/add-reference-update.test.d.ts +2 -0
- package/dist/cli/add-reference-update.test.d.ts.map +1 -0
- package/dist/cli/doctor-reference-duplicates.test.d.ts +2 -0
- package/dist/cli/doctor-reference-duplicates.test.d.ts.map +1 -0
- package/dist/cli/index.js +366 -103
- package/dist/cli/profile-reads.test.d.ts +2 -0
- package/dist/cli/profile-reads.test.d.ts.map +1 -0
- package/dist/cli/profile-update.test.d.ts +2 -0
- package/dist/cli/profile-update.test.d.ts.map +1 -0
- package/dist/data/config-store.d.ts +10 -1
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/db/profiles.d.ts +4 -1
- package/dist/db/profiles.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +226 -30
- package/dist/lib/bounded-read.d.ts +7 -0
- package/dist/lib/bounded-read.d.ts.map +1 -0
- package/dist/lib/config-target-identity.d.ts +76 -0
- package/dist/lib/config-target-identity.d.ts.map +1 -1
- package/dist/lib/project-dashboard-standard.d.ts +1 -1
- package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
- package/dist/mcp/index.js +314 -127
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/server/index.js +246 -36
- package/dist/server/openapi.d.ts +246 -20
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/profile-contract.test.d.ts +2 -0
- package/dist/server/profile-contract.test.d.ts.map +1 -0
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/cloud-store.d.ts +8 -1
- package/dist/storage/cloud-store.d.ts.map +1 -1
- package/dist/types/index.d.ts +24 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2649,6 +2649,84 @@ var init_machine = __esm(() => {
|
|
|
2649
2649
|
init_template();
|
|
2650
2650
|
});
|
|
2651
2651
|
|
|
2652
|
+
// src/lib/compact-output.ts
|
|
2653
|
+
function parseLimit(value, fallback = DEFAULT_LIST_LIMIT, max = MAX_LIST_LIMIT) {
|
|
2654
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
2655
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
2656
|
+
return fallback;
|
|
2657
|
+
return Math.min(Math.floor(parsed), max);
|
|
2658
|
+
}
|
|
2659
|
+
function parseCursor(value) {
|
|
2660
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
2661
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
2662
|
+
return 0;
|
|
2663
|
+
return Math.floor(parsed);
|
|
2664
|
+
}
|
|
2665
|
+
function paginate(items, opts = {}) {
|
|
2666
|
+
const limit = parseLimit(opts.limit, opts.defaultLimit ?? DEFAULT_LIST_LIMIT, opts.maxLimit ?? MAX_LIST_LIMIT);
|
|
2667
|
+
const cursor = parseCursor(opts.cursor);
|
|
2668
|
+
const pageItems = items.slice(cursor, cursor + limit);
|
|
2669
|
+
const nextCursor = cursor + pageItems.length < items.length ? cursor + pageItems.length : null;
|
|
2670
|
+
return {
|
|
2671
|
+
items: pageItems,
|
|
2672
|
+
total: items.length,
|
|
2673
|
+
limit,
|
|
2674
|
+
cursor,
|
|
2675
|
+
next_cursor: nextCursor,
|
|
2676
|
+
has_more: nextCursor !== null
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
function truncateText(value, max = 80) {
|
|
2680
|
+
const text = (value ?? "").replace(/\s+/g, " ").trim();
|
|
2681
|
+
if (text.length <= max)
|
|
2682
|
+
return text;
|
|
2683
|
+
if (max <= 3)
|
|
2684
|
+
return text.slice(0, max);
|
|
2685
|
+
return `${text.slice(0, max - 3)}...`;
|
|
2686
|
+
}
|
|
2687
|
+
function truncateMiddle(value, max = 80) {
|
|
2688
|
+
const text = (value ?? "").replace(/\s+/g, " ").trim();
|
|
2689
|
+
if (text.length <= max)
|
|
2690
|
+
return text;
|
|
2691
|
+
if (max <= 3)
|
|
2692
|
+
return text.slice(0, max);
|
|
2693
|
+
const head = Math.ceil((max - 3) * 0.55);
|
|
2694
|
+
const tail = Math.floor((max - 3) * 0.45);
|
|
2695
|
+
return `${text.slice(0, head)}...${text.slice(text.length - tail)}`;
|
|
2696
|
+
}
|
|
2697
|
+
var DEFAULT_LIST_LIMIT = 20, MAX_LIST_LIMIT = 100;
|
|
2698
|
+
|
|
2699
|
+
// src/lib/bounded-read.ts
|
|
2700
|
+
function normalizeBoundedReadOptions(options = {}) {
|
|
2701
|
+
return {
|
|
2702
|
+
limit: parseLimit(options.limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT),
|
|
2703
|
+
cursor: parseCursor(options.cursor)
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
function boundedReadPage(items, total, options = {}) {
|
|
2707
|
+
const { limit, cursor } = normalizeBoundedReadOptions(options);
|
|
2708
|
+
if (items.length > limit) {
|
|
2709
|
+
throw new Error(`bounded read returned ${items.length} rows for limit ${limit}`);
|
|
2710
|
+
}
|
|
2711
|
+
const consumed = cursor + items.length;
|
|
2712
|
+
const complete = consumed >= total;
|
|
2713
|
+
if (!complete && items.length === 0) {
|
|
2714
|
+
throw new Error(`bounded read did not advance at cursor ${cursor} of ${total}`);
|
|
2715
|
+
}
|
|
2716
|
+
return {
|
|
2717
|
+
items,
|
|
2718
|
+
total,
|
|
2719
|
+
limit,
|
|
2720
|
+
cursor,
|
|
2721
|
+
next_cursor: complete ? null : consumed,
|
|
2722
|
+
has_more: !complete,
|
|
2723
|
+
complete,
|
|
2724
|
+
truncated: false,
|
|
2725
|
+
source_bounded: true
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
var init_bounded_read = () => {};
|
|
2729
|
+
|
|
2652
2730
|
// src/db/profiles.ts
|
|
2653
2731
|
function rowToProfile(row) {
|
|
2654
2732
|
return {
|
|
@@ -2692,9 +2770,12 @@ function getProfile(idOrSlug, db) {
|
|
|
2692
2770
|
throw new ProfileNotFoundError(idOrSlug);
|
|
2693
2771
|
return rowToProfile(row);
|
|
2694
2772
|
}
|
|
2695
|
-
function
|
|
2773
|
+
function listProfilesPage(options = {}, db) {
|
|
2696
2774
|
const d = db || getDatabase();
|
|
2697
|
-
|
|
2775
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
2776
|
+
const total = d.query("SELECT COUNT(*) AS total FROM profiles").get()?.total ?? 0;
|
|
2777
|
+
const rows = d.query("SELECT * FROM profiles ORDER BY name LIMIT ? OFFSET ?").all(normalized.limit, normalized.cursor).map(rowToProfile);
|
|
2778
|
+
return boundedReadPage(rows, total, normalized);
|
|
2698
2779
|
}
|
|
2699
2780
|
function updateProfile(idOrSlug, input, db) {
|
|
2700
2781
|
const d = db || getDatabase();
|
|
@@ -2739,14 +2820,13 @@ function removeConfigFromProfile(profileIdOrSlug, configId, db) {
|
|
|
2739
2820
|
const profile = getProfile(profileIdOrSlug, d);
|
|
2740
2821
|
d.run("DELETE FROM profile_configs WHERE profile_id = ? AND config_id = ?", [profile.id, configId]);
|
|
2741
2822
|
}
|
|
2742
|
-
function
|
|
2823
|
+
function getProfileConfigsPage(profileIdOrSlug, options = {}, db) {
|
|
2743
2824
|
const d = db || getDatabase();
|
|
2744
2825
|
const profile = getProfile(profileIdOrSlug, d);
|
|
2745
|
-
const
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
|
|
2826
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
2827
|
+
const total = d.query("SELECT COUNT(*) AS total FROM profile_configs WHERE profile_id = ?").get(profile.id)?.total ?? 0;
|
|
2828
|
+
const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order LIMIT ? OFFSET ?").all(profile.id, normalized.limit, normalized.cursor);
|
|
2829
|
+
return boundedReadPage(rows.map((row) => getConfigById(row.config_id, d)), total, normalized);
|
|
2750
2830
|
}
|
|
2751
2831
|
function profileHasSelectors(profile) {
|
|
2752
2832
|
const selectors = profile.selectors ?? {};
|
|
@@ -2762,20 +2842,46 @@ function profileMatchesMachine(profile, machine) {
|
|
|
2762
2842
|
const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
|
|
2763
2843
|
return osMatches && archMatches && hostnameMatches;
|
|
2764
2844
|
}
|
|
2765
|
-
function
|
|
2766
|
-
const
|
|
2767
|
-
const
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2845
|
+
function resolveProfileForMachineRead(machine = detectMachineContext(), options = {}, db) {
|
|
2846
|
+
const d = db || getDatabase();
|
|
2847
|
+
const { limit } = normalizeBoundedReadOptions(options);
|
|
2848
|
+
let cursor = 0;
|
|
2849
|
+
let scanned = 0;
|
|
2850
|
+
let total = 0;
|
|
2851
|
+
let selected = null;
|
|
2852
|
+
while (true) {
|
|
2853
|
+
const page = listProfilesPage({ limit, cursor }, d);
|
|
2854
|
+
total = page.total;
|
|
2855
|
+
scanned += page.items.length;
|
|
2856
|
+
for (const profile of page.items) {
|
|
2857
|
+
if (!profileHasSelectors(profile) || !profileMatchesMachine(profile, machine))
|
|
2858
|
+
continue;
|
|
2859
|
+
const selectors = profile.selectors;
|
|
2860
|
+
const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
|
|
2861
|
+
if (!selected || score > selected.score || score === selected.score && profile.name.localeCompare(selected.profile.name) < 0) {
|
|
2862
|
+
selected = { profile, score };
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
if (page.complete)
|
|
2866
|
+
break;
|
|
2867
|
+
cursor = page.next_cursor;
|
|
2868
|
+
}
|
|
2869
|
+
return {
|
|
2870
|
+
profile: selected?.profile ?? null,
|
|
2871
|
+
scanned,
|
|
2872
|
+
total,
|
|
2873
|
+
batch_limit: limit,
|
|
2874
|
+
source_bounded: true,
|
|
2875
|
+
complete: true,
|
|
2876
|
+
truncated: false
|
|
2877
|
+
};
|
|
2773
2878
|
}
|
|
2774
2879
|
var init_profiles = __esm(() => {
|
|
2775
2880
|
init_types();
|
|
2776
2881
|
init_database();
|
|
2777
2882
|
init_configs();
|
|
2778
2883
|
init_machine();
|
|
2884
|
+
init_bounded_read();
|
|
2779
2885
|
});
|
|
2780
2886
|
|
|
2781
2887
|
// src/db/snapshots.ts
|
|
@@ -2853,6 +2959,32 @@ var init_machines = __esm(() => {
|
|
|
2853
2959
|
|
|
2854
2960
|
// src/data/config-store.ts
|
|
2855
2961
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
2962
|
+
function parseBoundedPagePayload(value, label) {
|
|
2963
|
+
const page = value;
|
|
2964
|
+
const consumed = Number(page?.cursor) + (page?.items?.length ?? 0);
|
|
2965
|
+
const complete = Boolean(page && Number.isSafeInteger(page.total) && consumed >= Number(page.total));
|
|
2966
|
+
if (!page || !Array.isArray(page.items) || !Number.isSafeInteger(page.total) || Number(page.total) < 0 || !Number.isSafeInteger(page.limit) || Number(page.limit) < 1 || !Number.isSafeInteger(page.cursor) || Number(page.cursor) < 0 || page.items.length > Number(page.limit) || typeof page.has_more !== "boolean" || typeof page.complete !== "boolean" || page.truncated !== false || page.next_cursor !== null && !Number.isSafeInteger(page.next_cursor) || page.complete !== complete || page.has_more !== !complete || page.next_cursor !== (complete ? null : consumed)) {
|
|
2967
|
+
throw new CloudHttpError(502, `${label} returned an invalid or truncated bounded-read envelope`, value);
|
|
2968
|
+
}
|
|
2969
|
+
return {
|
|
2970
|
+
...page,
|
|
2971
|
+
source_bounded: page.source_bounded ?? true
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
|
|
2975
|
+
if (value && typeof value === "object") {
|
|
2976
|
+
const candidate = value;
|
|
2977
|
+
if ("items" in candidate || "total" in candidate || "complete" in candidate || "truncated" in candidate || "next_cursor" in candidate) {
|
|
2978
|
+
return parseBoundedPagePayload(value, label);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
if (!Array.isArray(legacyItems)) {
|
|
2982
|
+
throw new CloudHttpError(502, `${label} returned neither a bounded envelope nor a complete legacy array`, value);
|
|
2983
|
+
}
|
|
2984
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
2985
|
+
const page = boundedReadPage(legacyItems.slice(normalized.cursor, normalized.cursor + normalized.limit), legacyItems.length, normalized);
|
|
2986
|
+
return { ...page, source_bounded: false };
|
|
2987
|
+
}
|
|
2856
2988
|
function isCloudAuthError(err) {
|
|
2857
2989
|
return err instanceof CloudHttpError && (err.status === 401 || err.status === 403);
|
|
2858
2990
|
}
|
|
@@ -2930,13 +3062,35 @@ class LocalConfigStore {
|
|
|
2930
3062
|
return pruneSnapshots(configId, keep, this.db);
|
|
2931
3063
|
}
|
|
2932
3064
|
async listProfiles() {
|
|
2933
|
-
|
|
3065
|
+
const profiles = [];
|
|
3066
|
+
let cursor = 0;
|
|
3067
|
+
while (true) {
|
|
3068
|
+
const page = await this.listProfilesPage({ limit: 100, cursor });
|
|
3069
|
+
profiles.push(...page.items);
|
|
3070
|
+
if (page.complete)
|
|
3071
|
+
return profiles;
|
|
3072
|
+
cursor = page.next_cursor;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
async listProfilesPage(options = {}) {
|
|
3076
|
+
return listProfilesPage(options, this.db);
|
|
2934
3077
|
}
|
|
2935
3078
|
async getProfile(idOrSlug) {
|
|
2936
3079
|
return getProfile(idOrSlug, this.db);
|
|
2937
3080
|
}
|
|
2938
3081
|
async getProfileConfigs(idOrSlug) {
|
|
2939
|
-
|
|
3082
|
+
const configs = [];
|
|
3083
|
+
let cursor = 0;
|
|
3084
|
+
while (true) {
|
|
3085
|
+
const page = await this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor });
|
|
3086
|
+
configs.push(...page.items);
|
|
3087
|
+
if (page.complete)
|
|
3088
|
+
return configs;
|
|
3089
|
+
cursor = page.next_cursor;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
async getProfileConfigsPage(idOrSlug, options = {}) {
|
|
3093
|
+
return getProfileConfigsPage(idOrSlug, options, this.db);
|
|
2940
3094
|
}
|
|
2941
3095
|
async createProfile(input) {
|
|
2942
3096
|
return createProfile(input, this.db);
|
|
@@ -2954,7 +3108,10 @@ class LocalConfigStore {
|
|
|
2954
3108
|
removeConfigFromProfile(profileIdOrSlug, configId, this.db);
|
|
2955
3109
|
}
|
|
2956
3110
|
async resolveProfileForMachine(machine) {
|
|
2957
|
-
return
|
|
3111
|
+
return (await this.resolveProfileForMachineRead(machine)).profile;
|
|
3112
|
+
}
|
|
3113
|
+
async resolveProfileForMachineRead(machine, options = {}) {
|
|
3114
|
+
return machine ? resolveProfileForMachineRead(machine, options, this.db) : resolveProfileForMachineRead(undefined, options, this.db);
|
|
2958
3115
|
}
|
|
2959
3116
|
async registerMachine(hostname2, os, arch2) {
|
|
2960
3117
|
return registerMachine(hostname2, os, arch2, this.db);
|
|
@@ -3095,8 +3252,24 @@ class CloudConfigStore {
|
|
|
3095
3252
|
return data?.pruned ?? 0;
|
|
3096
3253
|
}
|
|
3097
3254
|
async listProfiles() {
|
|
3098
|
-
const
|
|
3099
|
-
|
|
3255
|
+
const profiles = [];
|
|
3256
|
+
let cursor = 0;
|
|
3257
|
+
while (true) {
|
|
3258
|
+
const page = await this.listProfilesPage({ limit: 100, cursor });
|
|
3259
|
+
profiles.push(...page.items);
|
|
3260
|
+
if (page.complete)
|
|
3261
|
+
return profiles;
|
|
3262
|
+
cursor = page.next_cursor;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
async listProfilesPage(options = {}) {
|
|
3266
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
3267
|
+
const params = new URLSearchParams;
|
|
3268
|
+
params.set("limit", String(normalized.limit));
|
|
3269
|
+
params.set("cursor", String(normalized.cursor));
|
|
3270
|
+
const qs = params.toString();
|
|
3271
|
+
const { data } = await this.request("GET", `/profiles${qs ? `?${qs}` : ""}`);
|
|
3272
|
+
return parseBoundedOrLegacyPage(data, data?.profiles, normalized, "profile list");
|
|
3100
3273
|
}
|
|
3101
3274
|
async getProfile(idOrSlug) {
|
|
3102
3275
|
const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
@@ -3106,10 +3279,26 @@ class CloudConfigStore {
|
|
|
3106
3279
|
return profile;
|
|
3107
3280
|
}
|
|
3108
3281
|
async getProfileConfigs(idOrSlug) {
|
|
3109
|
-
const
|
|
3282
|
+
const configs = [];
|
|
3283
|
+
let cursor = 0;
|
|
3284
|
+
while (true) {
|
|
3285
|
+
const page = await this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor });
|
|
3286
|
+
configs.push(...page.items);
|
|
3287
|
+
if (page.complete)
|
|
3288
|
+
return configs;
|
|
3289
|
+
cursor = page.next_cursor;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
async getProfileConfigsPage(idOrSlug, options = {}) {
|
|
3293
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
3294
|
+
const params = new URLSearchParams;
|
|
3295
|
+
params.set("limit", String(normalized.limit));
|
|
3296
|
+
params.set("cursor", String(normalized.cursor));
|
|
3297
|
+
const qs = params.toString();
|
|
3298
|
+
const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
|
|
3110
3299
|
if (status === 404 || !data?.profile)
|
|
3111
3300
|
throw new ProfileNotFoundError(idOrSlug);
|
|
3112
|
-
return data.profile.configs
|
|
3301
|
+
return parseBoundedOrLegacyPage(data.configs, data.profile.configs, normalized, "profile membership");
|
|
3113
3302
|
}
|
|
3114
3303
|
async createProfile(input) {
|
|
3115
3304
|
const { data } = await this.request("POST", "/profiles", input, {
|
|
@@ -3133,6 +3322,10 @@ class CloudConfigStore {
|
|
|
3133
3322
|
await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
|
|
3134
3323
|
}
|
|
3135
3324
|
async resolveProfileForMachine(machine) {
|
|
3325
|
+
return (await this.resolveProfileForMachineRead(machine)).profile;
|
|
3326
|
+
}
|
|
3327
|
+
async resolveProfileForMachineRead(machine, options = {}) {
|
|
3328
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
3136
3329
|
const params = new URLSearchParams;
|
|
3137
3330
|
if (machine?.hostname)
|
|
3138
3331
|
params.set("hostname", machine.hostname);
|
|
@@ -3140,11 +3333,40 @@ class CloudConfigStore {
|
|
|
3140
3333
|
params.set("os", machine.os);
|
|
3141
3334
|
if (machine?.arch)
|
|
3142
3335
|
params.set("arch", machine.arch);
|
|
3336
|
+
params.set("limit", String(normalized.limit));
|
|
3143
3337
|
const qs = params.toString();
|
|
3144
3338
|
const { status, data } = await this.request("GET", `/profiles/resolve${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
|
|
3145
|
-
if (status === 404
|
|
3146
|
-
return
|
|
3147
|
-
|
|
3339
|
+
if (status === 404) {
|
|
3340
|
+
return {
|
|
3341
|
+
profile: null,
|
|
3342
|
+
scanned: null,
|
|
3343
|
+
total: null,
|
|
3344
|
+
batch_limit: null,
|
|
3345
|
+
source_bounded: false,
|
|
3346
|
+
complete: true,
|
|
3347
|
+
truncated: false
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
if (data && "complete" in data) {
|
|
3351
|
+
if (data.complete !== true || data.truncated !== false) {
|
|
3352
|
+
throw new CloudHttpError(502, "profile resolve returned an incomplete or truncated read", data);
|
|
3353
|
+
}
|
|
3354
|
+
return { ...data, source_bounded: data.source_bounded ?? true };
|
|
3355
|
+
}
|
|
3356
|
+
if (data && "profile" in data) {
|
|
3357
|
+
return {
|
|
3358
|
+
profile: data.profile,
|
|
3359
|
+
scanned: null,
|
|
3360
|
+
total: null,
|
|
3361
|
+
batch_limit: null,
|
|
3362
|
+
source_bounded: false,
|
|
3363
|
+
complete: true,
|
|
3364
|
+
truncated: false
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
{
|
|
3368
|
+
throw new CloudHttpError(502, "profile resolve returned an incomplete or truncated read", data);
|
|
3369
|
+
}
|
|
3148
3370
|
}
|
|
3149
3371
|
async registerMachine(hostname2, os, arch2) {
|
|
3150
3372
|
const { data } = await this.request("POST", "/machines", { hostname: hostname2, os, arch: arch2 }, { idempotent: true });
|
|
@@ -3181,6 +3403,7 @@ var init_config_store = __esm(() => {
|
|
|
3181
3403
|
init_machines();
|
|
3182
3404
|
init_database();
|
|
3183
3405
|
init_types();
|
|
3406
|
+
init_bounded_read();
|
|
3184
3407
|
CloudHttpError = class CloudHttpError extends Error {
|
|
3185
3408
|
status;
|
|
3186
3409
|
body;
|
|
@@ -13882,6 +14105,7 @@ import { basename as basename7, join as join15, resolve as resolve8 } from "path
|
|
|
13882
14105
|
|
|
13883
14106
|
// src/lib/config-target-identity.ts
|
|
13884
14107
|
init_apply();
|
|
14108
|
+
init_database();
|
|
13885
14109
|
function findConfigsByTargetPath(configs, targetPath) {
|
|
13886
14110
|
const wanted = normalizeTargetPath(targetPath);
|
|
13887
14111
|
return configs.filter((config) => {
|
|
@@ -13892,6 +14116,20 @@ function findConfigsByTargetPath(configs, targetPath) {
|
|
|
13892
14116
|
return normalizeTargetPath(config.target_path) === wanted;
|
|
13893
14117
|
});
|
|
13894
14118
|
}
|
|
14119
|
+
function findReferenceConfigsByName(configs, name) {
|
|
14120
|
+
const wantedSlug = slugify(name);
|
|
14121
|
+
return configs.filter((config) => config.kind === "reference" && (config.name === name || slugify(config.name) === wantedSlug));
|
|
14122
|
+
}
|
|
14123
|
+
function findDuplicateReferenceNameGroups(configs) {
|
|
14124
|
+
const groups = new Map;
|
|
14125
|
+
for (const config of configs) {
|
|
14126
|
+
if (config.kind !== "reference")
|
|
14127
|
+
continue;
|
|
14128
|
+
const key = slugify(config.name);
|
|
14129
|
+
groups.set(key, [...groups.get(key) ?? [], config]);
|
|
14130
|
+
}
|
|
14131
|
+
return [...groups.entries()].filter(([, rows]) => rows.length > 1).map(([, rows]) => ({ name: rows[0].name, configs: rows }));
|
|
14132
|
+
}
|
|
13895
14133
|
function findDuplicateTargetPathGroups(configs) {
|
|
13896
14134
|
const groups = new Map;
|
|
13897
14135
|
for (const config of configs) {
|
|
@@ -14921,7 +15159,7 @@ var PROJECT_DASHBOARD_PROFILE_VARIABLES = {
|
|
|
14921
15159
|
PROJECT_DASHBOARD_DIR: ".hasna/project",
|
|
14922
15160
|
PROJECT_DASHBOARD_RENDER_MANIFEST: ".hasna/project/dashboard/render.json",
|
|
14923
15161
|
PROJECT_DASHBOARD_SNAPSHOTS_DIR: ".hasna/project/dashboard/snapshots",
|
|
14924
|
-
PROJECT_CHANNEL_PREFIX: "
|
|
15162
|
+
PROJECT_CHANNEL_PREFIX: ""
|
|
14925
15163
|
};
|
|
14926
15164
|
var PROJECT_DASHBOARD_STANDARD_CONTENT = `# Agent-Managed Project Dashboard Standard
|
|
14927
15165
|
|
|
@@ -14976,8 +15214,8 @@ context.
|
|
|
14976
15214
|
|
|
14977
15215
|
## Coordination
|
|
14978
15216
|
|
|
14979
|
-
- Project conversation channels use
|
|
14980
|
-
displayed to humans as
|
|
15217
|
+
- Project conversation channels use the normalized project slug in the CLI and
|
|
15218
|
+
are displayed to humans as \`#<project-slug>\`.
|
|
14981
15219
|
- Todos tasks are the source of truth for work; messages are only coordination.
|
|
14982
15220
|
- Durable Codewith goal plans should own long-running implementation.
|
|
14983
15221
|
- New implementation/verification work should route through task-triggered fresh
|
|
@@ -15082,10 +15320,15 @@ function mergeProfileSelectors(preset, existing) {
|
|
|
15082
15320
|
};
|
|
15083
15321
|
}
|
|
15084
15322
|
function mergeProfileVariables(preset, existing) {
|
|
15085
|
-
|
|
15323
|
+
const variables = {
|
|
15086
15324
|
...preset ?? {},
|
|
15087
15325
|
...existing
|
|
15088
15326
|
};
|
|
15327
|
+
for (const [key, value] of Object.entries(preset ?? {})) {
|
|
15328
|
+
if (value === "")
|
|
15329
|
+
variables[key] = value;
|
|
15330
|
+
}
|
|
15331
|
+
return variables;
|
|
15089
15332
|
}
|
|
15090
15333
|
function mergeUnique(preset, existing) {
|
|
15091
15334
|
const values = [...new Set([...preset ?? [], ...existing ?? []])];
|
|
@@ -15388,56 +15631,6 @@ async function getConfigsStatus(store = resolveConfigStore()) {
|
|
|
15388
15631
|
|
|
15389
15632
|
// src/cli/index.tsx
|
|
15390
15633
|
init_config_store();
|
|
15391
|
-
|
|
15392
|
-
// src/lib/compact-output.ts
|
|
15393
|
-
var DEFAULT_LIST_LIMIT = 20;
|
|
15394
|
-
var MAX_LIST_LIMIT = 100;
|
|
15395
|
-
function parseLimit(value, fallback = DEFAULT_LIST_LIMIT, max = MAX_LIST_LIMIT) {
|
|
15396
|
-
const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
15397
|
-
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
15398
|
-
return fallback;
|
|
15399
|
-
return Math.min(Math.floor(parsed), max);
|
|
15400
|
-
}
|
|
15401
|
-
function parseCursor(value) {
|
|
15402
|
-
const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
15403
|
-
if (!Number.isFinite(parsed) || parsed < 0)
|
|
15404
|
-
return 0;
|
|
15405
|
-
return Math.floor(parsed);
|
|
15406
|
-
}
|
|
15407
|
-
function paginate(items, opts = {}) {
|
|
15408
|
-
const limit = parseLimit(opts.limit, opts.defaultLimit ?? DEFAULT_LIST_LIMIT, opts.maxLimit ?? MAX_LIST_LIMIT);
|
|
15409
|
-
const cursor = parseCursor(opts.cursor);
|
|
15410
|
-
const pageItems = items.slice(cursor, cursor + limit);
|
|
15411
|
-
const nextCursor = cursor + pageItems.length < items.length ? cursor + pageItems.length : null;
|
|
15412
|
-
return {
|
|
15413
|
-
items: pageItems,
|
|
15414
|
-
total: items.length,
|
|
15415
|
-
limit,
|
|
15416
|
-
cursor,
|
|
15417
|
-
next_cursor: nextCursor,
|
|
15418
|
-
has_more: nextCursor !== null
|
|
15419
|
-
};
|
|
15420
|
-
}
|
|
15421
|
-
function truncateText(value, max = 80) {
|
|
15422
|
-
const text = (value ?? "").replace(/\s+/g, " ").trim();
|
|
15423
|
-
if (text.length <= max)
|
|
15424
|
-
return text;
|
|
15425
|
-
if (max <= 3)
|
|
15426
|
-
return text.slice(0, max);
|
|
15427
|
-
return `${text.slice(0, max - 3)}...`;
|
|
15428
|
-
}
|
|
15429
|
-
function truncateMiddle(value, max = 80) {
|
|
15430
|
-
const text = (value ?? "").replace(/\s+/g, " ").trim();
|
|
15431
|
-
if (text.length <= max)
|
|
15432
|
-
return text;
|
|
15433
|
-
if (max <= 3)
|
|
15434
|
-
return text.slice(0, max);
|
|
15435
|
-
const head = Math.ceil((max - 3) * 0.55);
|
|
15436
|
-
const tail = Math.floor((max - 3) * 0.45);
|
|
15437
|
-
return `${text.slice(0, head)}...${text.slice(text.length - tail)}`;
|
|
15438
|
-
}
|
|
15439
|
-
|
|
15440
|
-
// src/cli/index.tsx
|
|
15441
15634
|
import { createRequire } from "module";
|
|
15442
15635
|
var pkg = createRequire(import.meta.url)("../../package.json");
|
|
15443
15636
|
var EAGAIN_SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
@@ -15665,6 +15858,15 @@ function parseVarArgs(values) {
|
|
|
15665
15858
|
}
|
|
15666
15859
|
return Object.keys(vars).length > 0 ? vars : undefined;
|
|
15667
15860
|
}
|
|
15861
|
+
function parseUnsetVarArgs(values) {
|
|
15862
|
+
if (!values || values.length === 0)
|
|
15863
|
+
return [];
|
|
15864
|
+
const keys = [...new Set(values.map((value) => value.trim()))];
|
|
15865
|
+
if (keys.some((key) => key.length === 0 || key.includes("="))) {
|
|
15866
|
+
throw new Error('Invalid --unset-var (expected variable names without "=")');
|
|
15867
|
+
}
|
|
15868
|
+
return keys;
|
|
15869
|
+
}
|
|
15668
15870
|
function parseProfileSelectors(opts) {
|
|
15669
15871
|
const selectors = {};
|
|
15670
15872
|
const os = splitCsv(opts.os);
|
|
@@ -15691,10 +15893,11 @@ function formatProfileSelectorSummary(profile) {
|
|
|
15691
15893
|
function formatProfileVariables(profile) {
|
|
15692
15894
|
return Object.entries(profile.variables).map(([key, value]) => `${key}=${value}`).join(", ");
|
|
15693
15895
|
}
|
|
15694
|
-
async function getMachineProfileContext(opts, store) {
|
|
15896
|
+
async function getMachineProfileContext(opts, store, readOptions = {}) {
|
|
15695
15897
|
const machine = detectMachineContext({ hostname: opts.hostname, os: opts.os, arch: opts.arch });
|
|
15696
|
-
const
|
|
15697
|
-
|
|
15898
|
+
const resolution = await store.resolveProfileForMachineRead(machine, readOptions);
|
|
15899
|
+
const profile = resolution.profile;
|
|
15900
|
+
return { machine, profile, resolution, vars: resolveProfileVariables(profile, machine) };
|
|
15698
15901
|
}
|
|
15699
15902
|
program.command("list").alias("ls").description("List stored configs").option("-c, --category <cat>", "filter by category").option("-a, --agent <agent>", "filter by agent").option("-k, --kind <kind>", "filter by kind (file|reference)").option("-t, --tag <tag>", "filter by tag").option("-s, --search <query>", "search name/description/content").option("-f, --format <fmt>", "output format: compact|table|json", "compact").option("--brief", "shorthand for --format compact").option("--verbose", "show expanded metadata for each listed config").option("--json", "output full matching records as JSON").option("--limit <n>", `max rows for human output (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "zero-based pagination cursor for human output").action(async (opts) => {
|
|
15700
15903
|
const fmt = opts.json ? "json" : opts.verbose ? "table" : opts.brief ? "compact" : opts.format;
|
|
@@ -15779,20 +15982,33 @@ program.command("add <path>").description("Ingest a file into the config DB").op
|
|
|
15779
15982
|
const targetPath = abs.startsWith(homedir7()) ? abs.replace(homedir7(), "~") : abs;
|
|
15780
15983
|
const name = opts.name || filePath.split("/").pop();
|
|
15781
15984
|
const store = resolveConfigStore();
|
|
15782
|
-
const
|
|
15985
|
+
const allConfigs = await store.listConfigs();
|
|
15986
|
+
const existingOwners = opts.kind === "reference" ? findReferenceConfigsByName(allConfigs, name) : findConfigsByTargetPath(allConfigs, targetPath);
|
|
15987
|
+
const isReference = opts.kind === "reference";
|
|
15988
|
+
const identityLabel = isReference ? `Reference config "${name}"` : targetPath;
|
|
15989
|
+
const identityNoun = isReference ? "name" : "path";
|
|
15783
15990
|
if (existingOwners.length > 0 && !opts.update) {
|
|
15784
15991
|
const owners = existingOwners.map((owner) => `${owner.slug} (${owner.id})`).join(", ");
|
|
15785
|
-
console.error(chalk.red(`${
|
|
15992
|
+
console.error(chalk.red(`${identityLabel} is already tracked by: ${owners}`));
|
|
15786
15993
|
if (existingOwners.length > 1) {
|
|
15787
|
-
console.error(chalk.red(` ${existingOwners.length} rows already collide on this
|
|
15994
|
+
console.error(chalk.red(` ${existingOwners.length} rows already collide on this ${identityNoun} \u2014 apply order between them is undefined.`));
|
|
15788
15995
|
}
|
|
15789
15996
|
console.error(chalk.dim(" Use `instructions add <path> --update` to refresh that row in place,"));
|
|
15790
|
-
|
|
15997
|
+
if (isReference) {
|
|
15998
|
+
console.error(chalk.dim(" or `instructions delete <id>` first."));
|
|
15999
|
+
} else {
|
|
16000
|
+
console.error(chalk.dim(" `instructions sync` to pull disk changes in, or `instructions delete <id>` first."));
|
|
16001
|
+
}
|
|
15791
16002
|
process.exit(1);
|
|
15792
16003
|
}
|
|
15793
16004
|
let config;
|
|
15794
16005
|
if (existingOwners.length > 0) {
|
|
15795
|
-
const
|
|
16006
|
+
const exactIndex = isReference ? existingOwners.findIndex((owner) => owner.name === name) : -1;
|
|
16007
|
+
const target = exactIndex >= 0 ? existingOwners[exactIndex] : existingOwners[0];
|
|
16008
|
+
const rest = existingOwners.filter((owner) => owner.id !== target.id);
|
|
16009
|
+
if (content !== target.content) {
|
|
16010
|
+
await store.createSnapshot(target.id, target.content, target.version);
|
|
16011
|
+
}
|
|
15796
16012
|
config = await store.updateConfig(target.id, {
|
|
15797
16013
|
content,
|
|
15798
16014
|
format: fmt,
|
|
@@ -15802,7 +16018,7 @@ program.command("add <path>").description("Ingest a file into the config DB").op
|
|
|
15802
16018
|
});
|
|
15803
16019
|
console.log(chalk.green("\u2713") + ` Updated: ${chalk.bold(config.name)} ${chalk.dim(`(${config.slug})`)}`);
|
|
15804
16020
|
if (rest.length > 0) {
|
|
15805
|
-
console.log(chalk.yellow(` \u26A0 ${rest.length} other row(s) still
|
|
16021
|
+
console.log(chalk.yellow(` \u26A0 ${rest.length} other row(s) still share this ${identityNoun}: ${rest.map((r) => r.slug).join(", ")}`));
|
|
15806
16022
|
console.log(chalk.yellow(" Apply order between them is undefined. Delete the extras."));
|
|
15807
16023
|
}
|
|
15808
16024
|
if (redacted.length > 0) {
|
|
@@ -16024,29 +16240,28 @@ program.command("whoami").description("Show setup summary").action(async () => {
|
|
|
16024
16240
|
}
|
|
16025
16241
|
});
|
|
16026
16242
|
var profileCmd = program.command("profile").description("Manage config profiles (named bundles)");
|
|
16027
|
-
profileCmd.command("list").description("List all profiles").option("--brief", "compact one-line output").option("-f, --format <fmt>", "compact|table|json", "compact").option("--verbose", "show expanded profile metadata").option("--json", "output full profiles as JSON").option("--limit <n>", `max rows
|
|
16243
|
+
profileCmd.command("list").description("List all profiles").option("--brief", "compact one-line output").option("-f, --format <fmt>", "compact|table|json", "compact").option("--verbose", "show expanded profile metadata").option("--json", "output full profiles as JSON").option("--limit <n>", `max rows requested from the source (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "zero-based source pagination cursor").action(async (opts) => {
|
|
16028
16244
|
const fmt = opts.json ? "json" : opts.verbose ? "table" : opts.brief ? "compact" : opts.format;
|
|
16029
16245
|
const store = resolveConfigStore();
|
|
16030
|
-
const
|
|
16246
|
+
const page = await store.listProfilesPage({ limit: opts.limit, cursor: opts.cursor });
|
|
16031
16247
|
if (fmt === "json") {
|
|
16032
|
-
printJson(
|
|
16248
|
+
printJson(page);
|
|
16033
16249
|
return;
|
|
16034
16250
|
}
|
|
16035
|
-
if (
|
|
16251
|
+
if (page.total === 0) {
|
|
16036
16252
|
console.log(chalk.dim("No profiles."));
|
|
16037
16253
|
return;
|
|
16038
16254
|
}
|
|
16039
|
-
const page = paginate(profiles, { limit: opts.limit, cursor: opts.cursor });
|
|
16040
16255
|
if (fmt === "compact")
|
|
16041
16256
|
console.log(`${pad("slug", 28)} ${pad("configs", 8)} ${pad("match", 36)} vars`);
|
|
16042
16257
|
for (const p of page.items) {
|
|
16258
|
+
const configCount = (await store.getProfileConfigsPage(p.id, { limit: 1 })).total;
|
|
16043
16259
|
if (fmt === "compact") {
|
|
16044
16260
|
const selectorSummary2 = formatProfileSelectorSummary(p);
|
|
16045
|
-
console.log(`${pad(p.slug, 28)} ${pad(String(
|
|
16261
|
+
console.log(`${pad(p.slug, 28)} ${pad(String(configCount), 8)} ${pad(selectorSummary2 || "-", 36)} ${Object.keys(p.variables).length}`);
|
|
16046
16262
|
continue;
|
|
16047
16263
|
}
|
|
16048
|
-
|
|
16049
|
-
console.log(`${chalk.bold(p.name)} ${chalk.dim(`(${p.slug})`)} \u2014 ${configs.length} config(s)`);
|
|
16264
|
+
console.log(`${chalk.bold(p.name)} ${chalk.dim(`(${p.slug})`)} \u2014 ${configCount} config(s)`);
|
|
16050
16265
|
if (p.description)
|
|
16051
16266
|
console.log(` ${chalk.dim(p.description)}`);
|
|
16052
16267
|
const selectorSummary = formatProfileSelectorSummary(p);
|
|
@@ -16067,11 +16282,40 @@ profileCmd.command("create <name>").description("Create a new profile").option("
|
|
|
16067
16282
|
});
|
|
16068
16283
|
console.log(chalk.green("\u2713") + ` Created profile: ${chalk.bold(p.name)} ${chalk.dim(`(${p.slug})`)}`);
|
|
16069
16284
|
});
|
|
16070
|
-
profileCmd.command("
|
|
16285
|
+
profileCmd.command("update <id>").description("Update an existing profile's variables in one store operation").option("--var <vars...>", "set profile variable(s) as KEY=VALUE").option("--unset-var <keys...>", "remove profile variable(s) by key").action(async (id, opts) => {
|
|
16286
|
+
try {
|
|
16287
|
+
const setVariables = parseVarArgs(opts.var) ?? {};
|
|
16288
|
+
const unsetVariables = parseUnsetVarArgs(opts.unsetVar);
|
|
16289
|
+
const setKeys = new Set(Object.keys(setVariables));
|
|
16290
|
+
const conflicts = unsetVariables.filter((key) => setKeys.has(key));
|
|
16291
|
+
if (conflicts.length > 0) {
|
|
16292
|
+
throw new Error(`Variables cannot be both set and unset: ${conflicts.join(", ")}`);
|
|
16293
|
+
}
|
|
16294
|
+
if (Object.keys(setVariables).length === 0 && unsetVariables.length === 0) {
|
|
16295
|
+
throw new Error("Provide --var KEY=VALUE and/or --unset-var KEY");
|
|
16296
|
+
}
|
|
16297
|
+
const store = resolveConfigStore();
|
|
16298
|
+
const profile = await store.getProfile(id);
|
|
16299
|
+
const variables = { ...profile.variables };
|
|
16300
|
+
for (const key of unsetVariables)
|
|
16301
|
+
delete variables[key];
|
|
16302
|
+
Object.assign(variables, setVariables);
|
|
16303
|
+
const updated = await store.updateProfile(profile.id, { variables });
|
|
16304
|
+
console.log(chalk.green("\u2713") + ` Updated profile: ${chalk.bold(updated.name)} ${chalk.dim(`(${updated.slug})`)}`);
|
|
16305
|
+
} catch (e) {
|
|
16306
|
+
console.error(chalk.red(formatCliError(e)));
|
|
16307
|
+
process.exit(1);
|
|
16308
|
+
}
|
|
16309
|
+
});
|
|
16310
|
+
profileCmd.command("show <id>").description("Show profile and its configs").option("--limit <n>", `max config rows (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "zero-based pagination cursor").option("--json", "output the profile and bounded membership page as JSON").action(async (id, opts) => {
|
|
16071
16311
|
try {
|
|
16072
16312
|
const store = resolveConfigStore();
|
|
16073
16313
|
const p = await store.getProfile(id);
|
|
16074
|
-
const
|
|
16314
|
+
const page = await store.getProfileConfigsPage(id, { limit: opts.limit, cursor: opts.cursor });
|
|
16315
|
+
if (opts.json) {
|
|
16316
|
+
printJson({ profile: p, configs: page });
|
|
16317
|
+
return;
|
|
16318
|
+
}
|
|
16075
16319
|
console.log(chalk.bold(p.name) + chalk.dim(` (${p.slug})`));
|
|
16076
16320
|
if (p.description)
|
|
16077
16321
|
console.log(chalk.dim(p.description));
|
|
@@ -16081,8 +16325,7 @@ profileCmd.command("show <id>").description("Show profile and its configs").opti
|
|
|
16081
16325
|
const varSummary = formatProfileVariables(p);
|
|
16082
16326
|
if (varSummary)
|
|
16083
16327
|
console.log(chalk.dim(`vars: ${varSummary}`));
|
|
16084
|
-
console.log(chalk.cyan(`${
|
|
16085
|
-
const page = paginate(configs, { limit: opts.limit, cursor: opts.cursor });
|
|
16328
|
+
console.log(chalk.cyan(`${page.total} config(s):`));
|
|
16086
16329
|
for (const c of page.items)
|
|
16087
16330
|
console.log(` ${c.slug} ${chalk.dim(`[${c.category}/${c.agent}]`)}`);
|
|
16088
16331
|
if (page.has_more) {
|
|
@@ -16159,9 +16402,15 @@ ${changed}/${results.length} changed (${selected.slug} on ${machine.hostname} ${
|
|
|
16159
16402
|
process.exit(1);
|
|
16160
16403
|
}
|
|
16161
16404
|
});
|
|
16162
|
-
profileCmd.command("resolve").description("Resolve the matching machine-aware profile").option("--hostname <hostname>", "override detected hostname").option("--os <os>", "override detected OS").option("--arch <arch>", "override detected arch").action(async (opts) => {
|
|
16405
|
+
profileCmd.command("resolve").description("Resolve the matching machine-aware profile").option("--hostname <hostname>", "override detected hostname").option("--os <os>", "override detected OS").option("--arch <arch>", "override detected arch").option("--limit <n>", `maximum profiles per source scan batch (default ${DEFAULT_LIST_LIMIT})`).option("--json", "output the complete bounded resolution read as JSON").action(async (opts) => {
|
|
16163
16406
|
const store = resolveConfigStore();
|
|
16164
|
-
const { machine, profile, vars } = await getMachineProfileContext(opts, store);
|
|
16407
|
+
const { machine, profile, resolution, vars } = await getMachineProfileContext(opts, store, { limit: opts.limit });
|
|
16408
|
+
if (opts.json) {
|
|
16409
|
+
printJson({ ...resolution, machine, vars });
|
|
16410
|
+
if (!profile)
|
|
16411
|
+
process.exitCode = 1;
|
|
16412
|
+
return;
|
|
16413
|
+
}
|
|
16165
16414
|
if (!profile) {
|
|
16166
16415
|
console.log(chalk.yellow(`No matching profile for ${machine.hostname} ${machine.os_family}/${machine.arch}`));
|
|
16167
16416
|
process.exit(1);
|
|
@@ -16843,6 +17092,20 @@ Stored configs (${allConfigs.length}):`));
|
|
|
16843
17092
|
}
|
|
16844
17093
|
console.log(chalk.dim(" Keep one row per path: `instructions delete <id>` for the extras."));
|
|
16845
17094
|
}
|
|
17095
|
+
const duplicateReferenceNames = findDuplicateReferenceNameGroups(allConfigs);
|
|
17096
|
+
if (duplicateReferenceNames.length === 0) {
|
|
17097
|
+
pass("No reference config name is claimed by more than one row");
|
|
17098
|
+
} else {
|
|
17099
|
+
const rowCount = duplicateReferenceNames.reduce((total, group) => total + group.configs.length, 0);
|
|
17100
|
+
fail(`${duplicateReferenceNames.length} reference name(s) claimed by more than one row (${rowCount} rows) \u2014 only one is live in the next render`);
|
|
17101
|
+
for (const group of duplicateReferenceNames) {
|
|
17102
|
+
console.log(chalk.yellow(` ${group.name}`));
|
|
17103
|
+
for (const c of group.configs) {
|
|
17104
|
+
console.log(chalk.dim(` ${c.slug} (${c.id}) updated ${c.updated_at}`));
|
|
17105
|
+
}
|
|
17106
|
+
}
|
|
17107
|
+
console.log(chalk.dim(" Keep one row per name: `instructions delete <id>` for the extras."));
|
|
17108
|
+
}
|
|
16846
17109
|
console.log(`
|
|
16847
17110
|
${issues === 0 ? chalk.green("\u2713 All checks passed") : chalk.yellow(`${issues} issue(s) found`)}`);
|
|
16848
17111
|
});
|