forkit-connect 0.1.20 → 0.1.22
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/dist/cli.js +1046 -188
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -87,9 +87,9 @@ const PUBLIC_COMMANDS = [
|
|
|
87
87
|
['scan', 'Discover runtimes, models, and agents on this device'],
|
|
88
88
|
['inbox', 'Review the Smart Registration Inbox'],
|
|
89
89
|
['sync', 'Flush queued drafts and lifecycle metadata'],
|
|
90
|
-
['workspace', '
|
|
91
|
-
['runtime', 'Register or review
|
|
92
|
-
['register', 'Register ready local models
|
|
90
|
+
['workspace', 'Optional governed workspace and project scope'],
|
|
91
|
+
['runtime', 'Register or review local runtimes for this repo/worktree'],
|
|
92
|
+
['register', 'Register ready local models'],
|
|
93
93
|
['ignore', 'Deny one detected local model on this device'],
|
|
94
94
|
['doctor', 'Run local environment diagnostics'],
|
|
95
95
|
];
|
|
@@ -117,86 +117,42 @@ const ADVANCED_COMMAND_GROUPS = [
|
|
|
117
117
|
['notify', 'Notification preview and delivery controls'],
|
|
118
118
|
];
|
|
119
119
|
function usage() {
|
|
120
|
-
|
|
121
|
-
console.log('
|
|
122
|
-
console.log('
|
|
123
|
-
console.log('
|
|
120
|
+
printCliHeader('CLI', 'Precise local review, runtime detection, and registration handoff.');
|
|
121
|
+
console.log(cliKeyLine('usage', 'forkit-connect <command> [options]'));
|
|
122
|
+
console.log(cliKeyLine('scope', 'forkit-connect workspace <list|select|create|status>'));
|
|
123
|
+
console.log(cliKeyLine('runtime', 'forkit-connect runtime <register|review|status>'));
|
|
124
|
+
printCliSection('Core');
|
|
124
125
|
for (const [command, description] of PUBLIC_COMMANDS) {
|
|
125
|
-
console.log(
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
console.log('
|
|
129
|
-
console.log('
|
|
130
|
-
console.log('
|
|
131
|
-
console.log('
|
|
132
|
-
console.log('
|
|
133
|
-
console.log('
|
|
134
|
-
console.log('
|
|
135
|
-
console.log('
|
|
136
|
-
console.log(' --subject-type <type> Runtime subject type used by runtime register');
|
|
137
|
-
console.log(' --deployment-environment <value> Runtime environment used by runtime register');
|
|
138
|
-
console.log(' --show-key Print the raw runtime API key when one is created');
|
|
139
|
-
console.log(' --no-api-key Skip automatic runtime API key creation');
|
|
140
|
-
console.log(' --dry-run Show the inferred runtime payload without creating it');
|
|
141
|
-
console.log(' --all-ready Register every ready local model in the current scope');
|
|
142
|
-
console.log(' --ignore-model <name> Deny a detected model on this device with --ignore-digest');
|
|
143
|
-
console.log(' --ignore-digest <sha> Digest used with --ignore-model');
|
|
144
|
-
console.log(' --digest <sha> Digest used by ignore when the model name is ambiguous');
|
|
145
|
-
console.log(' --interval-seconds <n> Override daemon scan interval');
|
|
146
|
-
console.log(' --no-browser Do not auto-open verification URL during login');
|
|
147
|
-
console.log(' --advanced-help Show internal/engineering commands');
|
|
126
|
+
console.log(cliKeyLine(command, description));
|
|
127
|
+
}
|
|
128
|
+
printCliSection('Flags');
|
|
129
|
+
console.log(cliKeyLine('--json', 'Machine-readable output when supported'));
|
|
130
|
+
console.log(cliKeyLine('--all-ready', 'Register every ready local model in the current scope'));
|
|
131
|
+
console.log(cliKeyLine('--model', 'Model name used by register'));
|
|
132
|
+
console.log(cliKeyLine('--entrypoint', 'Relative project path used by runtime register'));
|
|
133
|
+
console.log(cliKeyLine('--dry-run', 'Show inferred runtime payload without creating it'));
|
|
134
|
+
console.log(cliKeyLine('--no-browser', 'Do not auto-open browser verification during login'));
|
|
135
|
+
console.log(cliKeyLine('--plain', 'Disable the interactive terminal table surface'));
|
|
136
|
+
console.log(cliKeyLine('--advanced-help', 'Show internal and engineering commands'));
|
|
148
137
|
}
|
|
149
138
|
function advancedUsage() {
|
|
150
139
|
usage();
|
|
151
|
-
|
|
152
|
-
console.log('
|
|
153
|
-
console.log('
|
|
154
|
-
console.log('
|
|
155
|
-
console.log('
|
|
156
|
-
console.log('
|
|
157
|
-
console.log(' forkit-connect <review|workspaces|projects|bind|drafts|publish|bound|heartbeat|update-check|config|daemon|pulse|c2|train|agent|tray|notify> [options]');
|
|
140
|
+
printCliSection('Advanced');
|
|
141
|
+
console.log(cliKeyLine('connect', 'forkit-connect connect <modelNameOrDiscoveryHash>'));
|
|
142
|
+
console.log(cliKeyLine('connect', 'forkit-connect connect <start|init|status|inbox|services|permissions|handoff|evolution review|runtime review|runtime status>'));
|
|
143
|
+
console.log(cliKeyLine('targets', 'forkit-connect runtime target <add|list|remove|status>'));
|
|
144
|
+
console.log(cliKeyLine('observe', 'forkit-connect runtime observe --gaid <gaid>'));
|
|
145
|
+
console.log(cliKeyLine('more', 'forkit-connect <review|workspaces|projects|bind|drafts|publish|bound|heartbeat|update-check|config|daemon|pulse|c2|train|agent|tray|notify>'));
|
|
158
146
|
for (const [command, description] of ADVANCED_COMMAND_GROUPS) {
|
|
159
|
-
console.log(
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
console.log('
|
|
163
|
-
console.log('
|
|
164
|
-
console.log('
|
|
165
|
-
console.log('
|
|
166
|
-
console.log('
|
|
167
|
-
console.log('
|
|
168
|
-
console.log(' --task <value> Training task used by train init');
|
|
169
|
-
console.log(' --dataset-ref <value> Safe dataset reference used by train init');
|
|
170
|
-
console.log(' --type <value> Training lifecycle event type');
|
|
171
|
-
console.log(' --ref <value> Dataset reference used by train dataset');
|
|
172
|
-
console.log(' --change <value> Dataset change type used by train dataset');
|
|
173
|
-
console.log(' --value <number> Metric value used by train metric');
|
|
174
|
-
console.log(' --version <value> Version name used by train version');
|
|
175
|
-
console.log(' --reason <value> Version or lifecycle reason');
|
|
176
|
-
console.log(' --path <value> Artifact path used by train artifact');
|
|
177
|
-
console.log(' --hash-artifact Hash artifact contents for train artifact');
|
|
178
|
-
console.log(' --heartbeat-gaid <gaid> Queue heartbeat runtime signal event for GAID');
|
|
179
|
-
console.log(' --heartbeat-key <key> API key used for heartbeat runtime signal event');
|
|
180
|
-
console.log(' Also used by: c2 set-key (stores key + backfills events)');
|
|
181
|
-
console.log(' --gaid <gaid> Runtime or passport GAID used by runtime observe and c2 run-log emit');
|
|
182
|
-
console.log(' --api-key <key> Runtime signal API key used by runtime observe and c2 run-log emit');
|
|
183
|
-
console.log(' --repo-root <path> Repo root used by runtime target add (defaults to current repo/worktree)');
|
|
184
|
-
console.log(' --provider <name> Provider label used by c2 run-log emit');
|
|
185
|
-
console.log(' --service-name <name> Service/agent/workflow label used by c2 run-log emit');
|
|
186
|
-
console.log(' --prompt-tokens <n> Prompt tokens used by c2 run-log emit');
|
|
187
|
-
console.log(' --completion-tokens <n> Completion tokens used by c2 run-log emit');
|
|
188
|
-
console.log(' --client-name <name> Optional repo/runtime client label used by c2 run-log emit');
|
|
189
|
-
console.log(' --command-label <name> Operator label used by runtime observe and c2 run-log emit');
|
|
190
|
-
console.log(' --actor-labels <csv> Actor labels used by c2 run-log emit (for example: Codex,Claude)');
|
|
191
|
-
console.log(' --task-labels <csv> Task or chat labels used by c2 run-log emit');
|
|
192
|
-
console.log(' --folder-labels <csv> Relative folder labels used by c2 run-log emit');
|
|
193
|
-
console.log(' --file-labels <csv> Relative file labels used by c2 run-log emit');
|
|
194
|
-
console.log(' --model-labels <csv> Optional model labels used by c2 run-log emit');
|
|
195
|
-
console.log(' --cpu-percent <n> Scoped CPU percentage used by c2 run-log emit');
|
|
196
|
-
console.log(' --memory-mb <n> Scoped memory usage used by c2 run-log emit');
|
|
197
|
-
console.log(' --vram-mb <n> Device VRAM usage used by c2 run-log emit');
|
|
198
|
-
console.log(' --emit-ambient Include ambient tool detections in runtime observe JSON output');
|
|
199
|
-
console.log(' --draft-only Allow draft creation even when governed publish capacity is full');
|
|
147
|
+
console.log(cliKeyLine(command, description));
|
|
148
|
+
}
|
|
149
|
+
printCliSection('Advanced Flags');
|
|
150
|
+
console.log(cliKeyLine('--session-ref', 'Store or update backend session reference'));
|
|
151
|
+
console.log(cliKeyLine('--workspace', 'Store workspace binding'));
|
|
152
|
+
console.log(cliKeyLine('--project', 'Store project binding'));
|
|
153
|
+
console.log(cliKeyLine('--gaid', 'Runtime or passport GAID used by runtime observe'));
|
|
154
|
+
console.log(cliKeyLine('--api-key', 'Runtime signal API key used by runtime observe'));
|
|
155
|
+
console.log(cliKeyLine('--draft-only', 'Allow private continuation when governed publish is blocked'));
|
|
200
156
|
}
|
|
201
157
|
function showUsage() {
|
|
202
158
|
if (hasFlag('--advanced-help')) {
|
|
@@ -760,6 +716,13 @@ function formatScopeReferenceLabel(value, kind) {
|
|
|
760
716
|
}
|
|
761
717
|
return `selected on this device (${shortId(normalized)})`;
|
|
762
718
|
}
|
|
719
|
+
function formatScopeDetailValue(value, kind) {
|
|
720
|
+
const normalized = String(value || '').trim();
|
|
721
|
+
if (!normalized) {
|
|
722
|
+
return kind === 'workspace' ? 'not selected' : 'not selected';
|
|
723
|
+
}
|
|
724
|
+
return `${kind} ${shortId(normalized)}`;
|
|
725
|
+
}
|
|
763
726
|
function printUpdateCheckLines(lines, useErrorStream = false) {
|
|
764
727
|
for (const line of lines) {
|
|
765
728
|
if (useErrorStream) {
|
|
@@ -846,6 +809,207 @@ function printConnectInit(result) {
|
|
|
846
809
|
function printJson(value) {
|
|
847
810
|
console.log(JSON.stringify(value, null, 2));
|
|
848
811
|
}
|
|
812
|
+
const CLI_LABEL_WIDTH = 16;
|
|
813
|
+
const CLI_WIDTH_FALLBACK = 88;
|
|
814
|
+
const ANSI_ESCAPE_PATTERN = /\u001B\[[0-9;]*m/g;
|
|
815
|
+
function canRenderCliColor() {
|
|
816
|
+
return Boolean(node_process_1.stdout.isTTY && process.env.NO_COLOR !== '1' && process.env.TERM !== 'dumb');
|
|
817
|
+
}
|
|
818
|
+
function paintCli(text, code) {
|
|
819
|
+
if (!canRenderCliColor()) {
|
|
820
|
+
return text;
|
|
821
|
+
}
|
|
822
|
+
return `\u001B[${code}m${text}\u001B[0m`;
|
|
823
|
+
}
|
|
824
|
+
function cliBold(text) {
|
|
825
|
+
return paintCli(text, '1');
|
|
826
|
+
}
|
|
827
|
+
function cliDim(text) {
|
|
828
|
+
return paintCli(text, '2');
|
|
829
|
+
}
|
|
830
|
+
function cliAccent(text) {
|
|
831
|
+
return paintCli(text, '1;36');
|
|
832
|
+
}
|
|
833
|
+
function cliGood(text) {
|
|
834
|
+
return paintCli(text, '1;32');
|
|
835
|
+
}
|
|
836
|
+
function cliWarm(text) {
|
|
837
|
+
return paintCli(text, '1;33');
|
|
838
|
+
}
|
|
839
|
+
function cliDanger(text) {
|
|
840
|
+
return paintCli(text, '1;31');
|
|
841
|
+
}
|
|
842
|
+
function cliPlainWidth(text) {
|
|
843
|
+
return text.replace(ANSI_ESCAPE_PATTERN, '').length;
|
|
844
|
+
}
|
|
845
|
+
function truncateCliText(value, maxWidth) {
|
|
846
|
+
const normalized = String(value ?? '').trim();
|
|
847
|
+
if (normalized.length <= maxWidth) {
|
|
848
|
+
return normalized;
|
|
849
|
+
}
|
|
850
|
+
return `${normalized.slice(0, Math.max(0, maxWidth - 3)).trimEnd()}...`;
|
|
851
|
+
}
|
|
852
|
+
function getCliRenderWidth() {
|
|
853
|
+
const columns = node_process_1.stdout.columns;
|
|
854
|
+
if (Number.isFinite(columns) && Number(columns) > 0) {
|
|
855
|
+
return Math.max(72, Number(columns));
|
|
856
|
+
}
|
|
857
|
+
return CLI_WIDTH_FALLBACK;
|
|
858
|
+
}
|
|
859
|
+
function cliRule(char = '-') {
|
|
860
|
+
return cliDim(char.repeat(Math.max(24, Math.min(getCliRenderWidth(), CLI_WIDTH_FALLBACK))));
|
|
861
|
+
}
|
|
862
|
+
function cliTag(label, tone = 'muted') {
|
|
863
|
+
const rendered = `[${label}]`;
|
|
864
|
+
switch (tone) {
|
|
865
|
+
case 'accent':
|
|
866
|
+
return cliAccent(rendered);
|
|
867
|
+
case 'good':
|
|
868
|
+
return cliGood(rendered);
|
|
869
|
+
case 'warm':
|
|
870
|
+
return cliWarm(rendered);
|
|
871
|
+
case 'danger':
|
|
872
|
+
return cliDanger(rendered);
|
|
873
|
+
default:
|
|
874
|
+
return cliDim(rendered);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function cliKeyLine(label, value) {
|
|
878
|
+
return ` ${cliDim(`${label}`.padEnd(CLI_LABEL_WIDTH, ' '))} ${value}`;
|
|
879
|
+
}
|
|
880
|
+
function printCliHeader(title, subtitle) {
|
|
881
|
+
console.log(`[forkit-connect] ${cliBold(title)}`);
|
|
882
|
+
if (subtitle) {
|
|
883
|
+
console.log(cliDim(subtitle));
|
|
884
|
+
}
|
|
885
|
+
console.log(cliRule());
|
|
886
|
+
}
|
|
887
|
+
function printCliSection(title) {
|
|
888
|
+
console.log('');
|
|
889
|
+
console.log(cliBold(title.toUpperCase()));
|
|
890
|
+
}
|
|
891
|
+
function printCliEntry(title, options) {
|
|
892
|
+
const trimmedTitle = truncateCliText(title, 36);
|
|
893
|
+
const summaryWidth = Math.max(24, Math.min(48, getCliRenderWidth() - cliPlainWidth(trimmedTitle) - 22));
|
|
894
|
+
const pieces = [trimmedTitle];
|
|
895
|
+
if (options?.tag) {
|
|
896
|
+
pieces.push(cliTag(options.tag, options.tagTone ?? 'muted'));
|
|
897
|
+
}
|
|
898
|
+
if (options?.summary) {
|
|
899
|
+
pieces.push(cliDim('->'));
|
|
900
|
+
pieces.push(truncateCliText(options.summary, summaryWidth));
|
|
901
|
+
}
|
|
902
|
+
console.log(` > ${pieces.join(' ')}`);
|
|
903
|
+
if (options?.meta) {
|
|
904
|
+
console.log(` ${cliDim(options.meta)}`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
function joinCliSummary(parts) {
|
|
908
|
+
return parts.filter((value) => Boolean(value && String(value).trim())).join(cliDim(' | '));
|
|
909
|
+
}
|
|
910
|
+
function formatCliCompactCount(count, singular, plural) {
|
|
911
|
+
const noun = count === 1 ? singular : (plural || `${singular}s`);
|
|
912
|
+
return `${count} ${noun}`;
|
|
913
|
+
}
|
|
914
|
+
function formatCliAge(seconds) {
|
|
915
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
916
|
+
return 'fresh';
|
|
917
|
+
}
|
|
918
|
+
if (seconds >= 86400) {
|
|
919
|
+
return `${Math.round(seconds / 86400)}d old`;
|
|
920
|
+
}
|
|
921
|
+
if (seconds >= 3600) {
|
|
922
|
+
return `${Math.round(seconds / 3600)}h old`;
|
|
923
|
+
}
|
|
924
|
+
if (seconds >= 60) {
|
|
925
|
+
return `${Math.round(seconds / 60)}m old`;
|
|
926
|
+
}
|
|
927
|
+
return `${Math.round(seconds)}s old`;
|
|
928
|
+
}
|
|
929
|
+
function formatCliScopeLabel(workspaceId, projectId) {
|
|
930
|
+
const workspace = String(workspaceId || '').trim();
|
|
931
|
+
const project = String(projectId || '').trim();
|
|
932
|
+
if (workspace && project) {
|
|
933
|
+
return `${shortId(workspace)} / ${shortId(project)}`;
|
|
934
|
+
}
|
|
935
|
+
if (workspace) {
|
|
936
|
+
return `${shortId(workspace)} / project needed`;
|
|
937
|
+
}
|
|
938
|
+
return 'solo / no workspace';
|
|
939
|
+
}
|
|
940
|
+
function summarizeReviewQueueCounts(values) {
|
|
941
|
+
const parts = [
|
|
942
|
+
values.draftFirstCount ? formatCliCompactCount(values.draftFirstCount, 'private review') : null,
|
|
943
|
+
values.readyCount ? formatCliCompactCount(values.readyCount, 'ready item') : null,
|
|
944
|
+
values.needsReviewCount ? formatCliCompactCount(values.needsReviewCount, 'review item') : null,
|
|
945
|
+
values.connectedCount ? formatCliCompactCount(values.connectedCount, 'connected item') : null,
|
|
946
|
+
];
|
|
947
|
+
const joined = joinCliSummary(parts);
|
|
948
|
+
return joined || 'clear';
|
|
949
|
+
}
|
|
950
|
+
function formatCliEvidenceGlyph(confidence) {
|
|
951
|
+
switch (String(confidence || '').trim().toLowerCase()) {
|
|
952
|
+
case 'high':
|
|
953
|
+
return '●●●';
|
|
954
|
+
case 'medium':
|
|
955
|
+
return '●●○';
|
|
956
|
+
case 'low':
|
|
957
|
+
return '●○○';
|
|
958
|
+
default:
|
|
959
|
+
return '○○○';
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function formatCliEvidenceLabel(confidence) {
|
|
963
|
+
const normalized = String(confidence || '').trim().toLowerCase();
|
|
964
|
+
switch (normalized) {
|
|
965
|
+
case 'high':
|
|
966
|
+
return 'high';
|
|
967
|
+
case 'medium':
|
|
968
|
+
return 'medium';
|
|
969
|
+
case 'low':
|
|
970
|
+
return 'low';
|
|
971
|
+
default:
|
|
972
|
+
return normalized || 'unknown';
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function formatCliEvidenceCell(confidence) {
|
|
976
|
+
return `${formatCliEvidenceGlyph(confidence)} ${formatCliEvidenceLabel(confidence)}`;
|
|
977
|
+
}
|
|
978
|
+
function formatCliDetailLine(label, value, labelWidth = 12) {
|
|
979
|
+
const safeLabel = `${label}:`;
|
|
980
|
+
const paddedLabel = `${safeLabel}${' '.repeat(Math.max(1, labelWidth - safeLabel.length))}`;
|
|
981
|
+
const normalizedValue = value == null
|
|
982
|
+
? ''
|
|
983
|
+
: typeof value === 'string'
|
|
984
|
+
? value.trim()
|
|
985
|
+
: String(value);
|
|
986
|
+
return `${paddedLabel}${normalizedValue || normalizedValue === '0' ? normalizedValue : 'n/a'}`;
|
|
987
|
+
}
|
|
988
|
+
function formatRuntimeRecommendedActionLabel(action) {
|
|
989
|
+
const normalized = String(action || '').trim().toLowerCase();
|
|
990
|
+
if (normalized === 'link an agent to a model on this runtime.') {
|
|
991
|
+
return 'Link agent to model';
|
|
992
|
+
}
|
|
993
|
+
if (normalized === 'connect a model served by this runtime.') {
|
|
994
|
+
return 'Register served model';
|
|
995
|
+
}
|
|
996
|
+
if (normalized === 'check whether this runtime should be running locally.') {
|
|
997
|
+
return 'Check local runtime';
|
|
998
|
+
}
|
|
999
|
+
return truncateCliText(action, 36);
|
|
1000
|
+
}
|
|
1001
|
+
function getDraftFirstCountFromInbox(inbox) {
|
|
1002
|
+
return inbox.groups.ready_to_connect.filter((item) => (item.item_type === 'model'
|
|
1003
|
+
&& item.recommended_action === 'create_passport_draft'
|
|
1004
|
+
&& String(item.details_received_automatically.registration_flow_state || '').trim() === 'private_draft_required')).length;
|
|
1005
|
+
}
|
|
1006
|
+
function splitReadyInboxItems(inbox) {
|
|
1007
|
+
const privateReview = inbox.groups.ready_to_connect.filter((item) => (item.item_type === 'model'
|
|
1008
|
+
&& item.recommended_action === 'create_passport_draft'
|
|
1009
|
+
&& String(item.details_received_automatically.registration_flow_state || '').trim() === 'private_draft_required'));
|
|
1010
|
+
const ready = inbox.groups.ready_to_connect.filter((item) => !privateReview.includes(item));
|
|
1011
|
+
return { privateReview, ready };
|
|
1012
|
+
}
|
|
849
1013
|
const INTERACTIVE_LABEL_WIDTH = 24;
|
|
850
1014
|
const INTERACTIVE_DISCOVERY_TIMEOUT_MS = 800;
|
|
851
1015
|
const INTERACTIVE_BINDING_TIMEOUT_MS = 800;
|
|
@@ -1235,42 +1399,65 @@ function printCollectedChanges(service, limit) {
|
|
|
1235
1399
|
for (const event of state.evidence_events) {
|
|
1236
1400
|
evidenceTypeCounts.set(event.type, (evidenceTypeCounts.get(event.type) || 0) + 1);
|
|
1237
1401
|
}
|
|
1238
|
-
|
|
1239
|
-
console.log(
|
|
1240
|
-
console.log(
|
|
1241
|
-
console.log(
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
console.log(
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1402
|
+
printCliHeader('Changes', 'Local evidence, queue, and runtime activity collected on this device.');
|
|
1403
|
+
console.log(cliKeyLine('state dir', paths.stateDir));
|
|
1404
|
+
console.log(cliKeyLine('state file', paths.stateFile));
|
|
1405
|
+
console.log(cliKeyLine('summary', joinCliSummary([
|
|
1406
|
+
formatCliCompactCount(state.evidence_events.length, 'evidence event'),
|
|
1407
|
+
formatCliCompactCount(state.pulse_events.length, 'pulse event'),
|
|
1408
|
+
formatCliCompactCount(state.sync_queue.length, 'queued sync'),
|
|
1409
|
+
formatCliCompactCount(state.pending_reviews.length, 'pending review'),
|
|
1410
|
+
])));
|
|
1411
|
+
console.log(cliKeyLine('local', joinCliSummary([
|
|
1412
|
+
formatCliCompactCount(state.detected_models.length, 'model'),
|
|
1413
|
+
formatCliCompactCount(state.detected_runtimes.length, 'runtime'),
|
|
1414
|
+
formatCliCompactCount(state.detected_agents.length, 'agent'),
|
|
1415
|
+
formatCliCompactCount(state.c2_events.length, 'c2 event'),
|
|
1416
|
+
])));
|
|
1417
|
+
console.log(cliKeyLine('note', 'Only synced queue items become backend-authoritative.'));
|
|
1251
1418
|
const sortedEvidenceTypes = [...evidenceTypeCounts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
|
|
1252
1419
|
if (sortedEvidenceTypes.length > 0) {
|
|
1253
|
-
|
|
1420
|
+
printCliSection('Evidence Types');
|
|
1254
1421
|
for (const [type, count] of sortedEvidenceTypes) {
|
|
1255
|
-
|
|
1422
|
+
printCliEntry(type, {
|
|
1423
|
+
summary: formatCliCompactCount(count, 'event'),
|
|
1424
|
+
});
|
|
1256
1425
|
}
|
|
1257
1426
|
}
|
|
1258
1427
|
if (recentEvidence.length > 0) {
|
|
1259
|
-
|
|
1428
|
+
printCliSection(`Recent Evidence ${cliTag(String(recentEvidence.length), 'accent')}`);
|
|
1260
1429
|
for (const event of recentEvidence) {
|
|
1261
|
-
|
|
1430
|
+
printCliEntry(event.type, {
|
|
1431
|
+
summary: formatTimestamp(event.createdAt),
|
|
1432
|
+
meta: truncateCliText(formatEvidenceDetails(event.details).replace(/^\s*\|\s*/, ''), 88),
|
|
1433
|
+
});
|
|
1262
1434
|
}
|
|
1263
1435
|
}
|
|
1264
1436
|
if (recentPulse.length > 0) {
|
|
1265
|
-
|
|
1437
|
+
printCliSection(`Runtime Signals ${cliTag(String(recentPulse.length), 'good')}`);
|
|
1266
1438
|
for (const event of recentPulse) {
|
|
1267
|
-
|
|
1439
|
+
printCliEntry(event.runtime_name, {
|
|
1440
|
+
summary: event.pulse_status,
|
|
1441
|
+
meta: joinCliSummary([
|
|
1442
|
+
formatTimestamp(event.measured_at),
|
|
1443
|
+
event.model_name ? `model ${event.model_name}` : null,
|
|
1444
|
+
formatPulseRegistrationLabel(event),
|
|
1445
|
+
]),
|
|
1446
|
+
});
|
|
1268
1447
|
}
|
|
1269
1448
|
}
|
|
1270
1449
|
if (state.sync_queue.length > 0) {
|
|
1271
|
-
|
|
1450
|
+
printCliSection(`Pending Sync ${cliTag(String(Math.min(state.sync_queue.length, limit)), 'warm')}`);
|
|
1272
1451
|
for (const item of state.sync_queue.slice(0, limit)) {
|
|
1273
|
-
|
|
1452
|
+
printCliEntry(item.type, {
|
|
1453
|
+
summary: item.endpoint,
|
|
1454
|
+
meta: joinCliSummary([
|
|
1455
|
+
`attempts ${item.attempts}`,
|
|
1456
|
+
`created ${formatTimestamp(item.createdAt)}`,
|
|
1457
|
+
item.nextRetryAt ? `retry ${formatTimestamp(item.nextRetryAt)}` : null,
|
|
1458
|
+
item.lastError ? `error ${truncateCliText(item.lastError, 48)}` : null,
|
|
1459
|
+
]),
|
|
1460
|
+
});
|
|
1274
1461
|
}
|
|
1275
1462
|
}
|
|
1276
1463
|
}
|
|
@@ -1304,14 +1491,25 @@ function printReviewSnapshot(snapshot) {
|
|
|
1304
1491
|
'shadow_candidate',
|
|
1305
1492
|
'provider_unavailable',
|
|
1306
1493
|
];
|
|
1307
|
-
|
|
1494
|
+
printCliHeader('Review', `${formatCliCompactCount(snapshot.total, 'local item')} across models and runtimes.`);
|
|
1308
1495
|
for (const status of order) {
|
|
1309
1496
|
const entries = snapshot.groups[status];
|
|
1310
1497
|
if (entries.length === 0)
|
|
1311
1498
|
continue;
|
|
1312
|
-
|
|
1499
|
+
printCliSection(`${formatReviewStatusLabel(status)} ${cliTag(String(entries.length), status === 'bound_passport' ? 'good' : status === 'provider_unavailable' ? 'danger' : 'warm')}`);
|
|
1313
1500
|
for (const entry of entries) {
|
|
1314
|
-
|
|
1501
|
+
printCliEntry(formatCliReviewSubject(entry), {
|
|
1502
|
+
tag: entry.kind,
|
|
1503
|
+
tagTone: entry.kind === 'provider' ? 'danger' : entry.kind === 'process' ? 'warm' : 'accent',
|
|
1504
|
+
summary: formatReviewSuggestedActionCompact(entry.suggested_action),
|
|
1505
|
+
meta: joinCliSummary([
|
|
1506
|
+
formatCliReviewRuntime(entry),
|
|
1507
|
+
entry.discovery_hash ? `discovery ${shortId(entry.discovery_hash)}` : null,
|
|
1508
|
+
entry.registration_key ? `registration ${shortId(entry.registration_key)}` : null,
|
|
1509
|
+
entry.passport_gaid ? `passport ${shortId(entry.passport_gaid)}` : null,
|
|
1510
|
+
formatReviewEvidence(entry).replace(/^\s*\|\s*/, ''),
|
|
1511
|
+
]),
|
|
1512
|
+
});
|
|
1315
1513
|
}
|
|
1316
1514
|
}
|
|
1317
1515
|
}
|
|
@@ -1340,17 +1538,93 @@ function printTrainStatus(status) {
|
|
|
1340
1538
|
}
|
|
1341
1539
|
function printAgentReview(snapshot) {
|
|
1342
1540
|
const order = ['new_agent', 'known_agent', 'linked_agent', 'inactive_agent', 'unknown_ai_tool'];
|
|
1343
|
-
|
|
1541
|
+
printCliHeader('Agent Review', `${formatCliCompactCount(snapshot.total, 'agent item')} detected on this device.`);
|
|
1344
1542
|
for (const status of order) {
|
|
1345
1543
|
const entries = snapshot.groups[status];
|
|
1346
1544
|
if (entries.length === 0)
|
|
1347
1545
|
continue;
|
|
1348
|
-
|
|
1546
|
+
const tone = status === 'linked_agent' ? 'good' : status === 'inactive_agent' ? 'danger' : status === 'unknown_ai_tool' ? 'muted' : 'warm';
|
|
1547
|
+
const label = status === 'new_agent'
|
|
1548
|
+
? 'New agents'
|
|
1549
|
+
: status === 'known_agent'
|
|
1550
|
+
? 'Needs link'
|
|
1551
|
+
: status === 'linked_agent'
|
|
1552
|
+
? 'Linked agents'
|
|
1553
|
+
: status === 'inactive_agent'
|
|
1554
|
+
? 'Inactive agents'
|
|
1555
|
+
: 'Unknown AI tools';
|
|
1556
|
+
printCliSection(`${label} ${cliTag(String(entries.length), tone)}`);
|
|
1349
1557
|
for (const entry of entries) {
|
|
1350
|
-
|
|
1558
|
+
printCliEntry(entry.agent_name, {
|
|
1559
|
+
tag: entry.agent_type.replaceAll('_', ' '),
|
|
1560
|
+
tagTone: entry.agent_type === 'unknown_ai_tool' ? 'muted' : 'accent',
|
|
1561
|
+
summary: formatAgentSuggestedActionCompact(entry.suggested_action),
|
|
1562
|
+
meta: joinCliSummary([
|
|
1563
|
+
entry.status.replaceAll('_', ' '),
|
|
1564
|
+
entry.linked_model_name ? `model ${entry.linked_model_name}` : null,
|
|
1565
|
+
entry.linked_passport_gaid ? `passport ${shortId(entry.linked_passport_gaid)}` : null,
|
|
1566
|
+
formatReviewEvidence(entry).replace(/^\s*\|\s*/, ''),
|
|
1567
|
+
]),
|
|
1568
|
+
});
|
|
1351
1569
|
}
|
|
1352
1570
|
}
|
|
1353
1571
|
}
|
|
1572
|
+
function printWorkspaceListSurface(workspaces, options) {
|
|
1573
|
+
printCliHeader('Workspaces', `${formatCliCompactCount(workspaces.length, 'accessible workspace')} on this account.`);
|
|
1574
|
+
const identityBits = [
|
|
1575
|
+
options?.displayName || null,
|
|
1576
|
+
options?.accountEmail || null,
|
|
1577
|
+
options?.platformRole ? `role ${options.platformRole}` : null,
|
|
1578
|
+
];
|
|
1579
|
+
if (identityBits.some(Boolean)) {
|
|
1580
|
+
console.log(cliKeyLine('account', joinCliSummary(identityBits)));
|
|
1581
|
+
}
|
|
1582
|
+
if (options?.accountLimits) {
|
|
1583
|
+
console.log(cliKeyLine('plan', options.accountLimits.planName));
|
|
1584
|
+
console.log(cliKeyLine('capacity', joinCliSummary([
|
|
1585
|
+
formatRemainingLimit(options.accountLimits.workspaceLimit, options.accountLimits.workspacesUsed, 'workspace'),
|
|
1586
|
+
formatRemainingLimit(options.accountLimits.projectLimit, options.accountLimits.projectsUsed, 'project'),
|
|
1587
|
+
])));
|
|
1588
|
+
}
|
|
1589
|
+
if (workspaces.length === 0) {
|
|
1590
|
+
console.log(cliKeyLine('next', 'Create a workspace from forkit-connect workspace create.'));
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
printCliSection('Accessible');
|
|
1594
|
+
for (const workspace of workspaces) {
|
|
1595
|
+
printCliEntry(String(workspace.name || 'Unnamed workspace').trim() || 'Unnamed workspace', {
|
|
1596
|
+
tag: String(workspace.role || 'unknown').trim() || 'unknown',
|
|
1597
|
+
tagTone: 'accent',
|
|
1598
|
+
summary: formatWorkspaceStateCell(workspace),
|
|
1599
|
+
meta: joinCliSummary([
|
|
1600
|
+
String(workspace.id || workspace.gaid || workspace.passportGaid || 'unknown'),
|
|
1601
|
+
workspace.verificationStatus ? `verification ${workspace.verificationStatus}` : null,
|
|
1602
|
+
workspace.description ? truncateCliText(workspace.description, 52) : null,
|
|
1603
|
+
]),
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function printProjectListSurface(projects, workspaceId) {
|
|
1608
|
+
printCliHeader('Projects', `${formatCliCompactCount(projects.length, 'project')} inside workspace ${shortId(workspaceId)}.`);
|
|
1609
|
+
if (projects.length === 0) {
|
|
1610
|
+
console.log(cliKeyLine('next', 'Create the first project with forkit-connect workspace select --workspace <id> --project-name "<name>".'));
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
printCliSection('Available');
|
|
1614
|
+
for (const project of projects) {
|
|
1615
|
+
const passportCount = Number(project.passportCount);
|
|
1616
|
+
printCliEntry(String(project.name || 'Unnamed project').trim() || 'Unnamed project', {
|
|
1617
|
+
tag: String(project.status || 'active').trim() || 'active',
|
|
1618
|
+
tagTone: 'good',
|
|
1619
|
+
summary: Number.isFinite(passportCount) && passportCount >= 0 ? `${passportCount} passports` : 'passport count unavailable',
|
|
1620
|
+
meta: joinCliSummary([
|
|
1621
|
+
String(project.id || 'unknown'),
|
|
1622
|
+
project.updatedAt ? `updated ${formatTimestamp(project.updatedAt)}` : null,
|
|
1623
|
+
project.description ? truncateCliText(project.description, 52) : null,
|
|
1624
|
+
]),
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1354
1628
|
function printAgentStatus(status) {
|
|
1355
1629
|
console.log(JSON.stringify({
|
|
1356
1630
|
detected_agents: status.detectedAgents,
|
|
@@ -1365,23 +1639,47 @@ function printAgentLedger(summary) {
|
|
|
1365
1639
|
console.log(JSON.stringify(summary, null, 2));
|
|
1366
1640
|
}
|
|
1367
1641
|
function printRuntimeReview(summary) {
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1642
|
+
const ready = summary.runtimes.filter((runtime) => runtime.status !== 'unavailable' && runtime.health_status === 'detected');
|
|
1643
|
+
const attention = summary.runtimes.filter((runtime) => !ready.includes(runtime));
|
|
1644
|
+
printCliHeader('Runtime Review', `${formatCliCompactCount(summary.total_runtimes, 'runtime')} on this device`);
|
|
1645
|
+
console.log(cliKeyLine('summary', joinCliSummary([
|
|
1646
|
+
formatCliCompactCount(ready.length, 'healthy runtime'),
|
|
1647
|
+
formatCliCompactCount(attention.length, 'runtime needs review', 'runtimes need review'),
|
|
1648
|
+
])));
|
|
1649
|
+
for (const [label, items] of [['Healthy', ready], ['Needs review', attention]]) {
|
|
1650
|
+
if (items.length === 0) {
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
printCliSection(`${label} ${cliTag(String(items.length), label === 'Healthy' ? 'good' : 'warm')}`);
|
|
1654
|
+
for (const runtime of items) {
|
|
1655
|
+
printCliEntry(runtime.runtime_name, {
|
|
1656
|
+
tag: runtime.status.replaceAll('_', ' '),
|
|
1657
|
+
tagTone: runtime.status === 'unavailable' ? 'warm' : 'good',
|
|
1658
|
+
meta: joinCliSummary([
|
|
1659
|
+
`health ${runtime.health_status}`,
|
|
1660
|
+
formatCliCompactCount(runtime.linked_models_count, 'model'),
|
|
1661
|
+
formatCliCompactCount(runtime.linked_agents_count, 'agent'),
|
|
1662
|
+
`next ${formatRuntimeRecommendedActionLabel(runtime.recommended_action)}`,
|
|
1663
|
+
]),
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1372
1666
|
}
|
|
1373
1667
|
}
|
|
1374
1668
|
function printRuntimeStatus(status) {
|
|
1375
|
-
|
|
1376
|
-
console.log(
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
console.log(
|
|
1382
|
-
|
|
1669
|
+
printCliHeader('Runtime Status');
|
|
1670
|
+
console.log(cliKeyLine('runtimes', joinCliSummary([
|
|
1671
|
+
formatCliCompactCount(status.total_runtimes, 'total'),
|
|
1672
|
+
formatCliCompactCount(status.online_runtimes, 'online runtime'),
|
|
1673
|
+
formatCliCompactCount(status.offline_runtimes, 'offline runtime'),
|
|
1674
|
+
])));
|
|
1675
|
+
console.log(cliKeyLine('linking', joinCliSummary([
|
|
1676
|
+
formatCliCompactCount(status.linked_runtimes, 'linked runtime'),
|
|
1677
|
+
formatCliCompactCount(status.unlinked_runtimes, 'unlinked runtime'),
|
|
1678
|
+
formatCliCompactCount(status.unhealthy_runtimes, 'runtime needs review', 'runtimes need review'),
|
|
1679
|
+
])));
|
|
1680
|
+
console.log(cliKeyLine('sync', `${status.c2_pending_count} pending runtime events`));
|
|
1383
1681
|
if (status.latest_runtime_lifecycle_event) {
|
|
1384
|
-
console.log(
|
|
1682
|
+
console.log(cliKeyLine('latest', truncateCliText(status.latest_runtime_lifecycle_event, 60)));
|
|
1385
1683
|
}
|
|
1386
1684
|
}
|
|
1387
1685
|
function printRuntimeTargetList(service, options) {
|
|
@@ -1437,24 +1735,369 @@ function formatSmartInboxActionLabel(item) {
|
|
|
1437
1735
|
}
|
|
1438
1736
|
return formatSmartInboxActionValue(item.recommended_action, item.item_type, String(item.details_received_automatically.connectable_model_name || '').trim());
|
|
1439
1737
|
}
|
|
1738
|
+
function getInboxGroupTone(label) {
|
|
1739
|
+
switch (label) {
|
|
1740
|
+
case 'Private review':
|
|
1741
|
+
return 'accent';
|
|
1742
|
+
case 'Ready to connect':
|
|
1743
|
+
return 'good';
|
|
1744
|
+
case 'Needs review':
|
|
1745
|
+
return 'warm';
|
|
1746
|
+
case 'Denied on this device':
|
|
1747
|
+
return 'danger';
|
|
1748
|
+
default:
|
|
1749
|
+
return 'muted';
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
function formatInboxItemMeta(item) {
|
|
1753
|
+
const match = item.matched_passport_gaid ? `match ${shortId(item.matched_passport_gaid)}` : null;
|
|
1754
|
+
const runtime = String(item.details_received_automatically.runtime_name || '').trim();
|
|
1755
|
+
const displayName = String(item.display_name || '').trim().toLowerCase();
|
|
1756
|
+
const sourceCandidate = item.item_type === 'runtime'
|
|
1757
|
+
? String(item.details_received_automatically.source_endpoint_label || runtime || '').trim()
|
|
1758
|
+
: '';
|
|
1759
|
+
const sourceNormalized = sourceCandidate.toLowerCase();
|
|
1760
|
+
const runtimeNormalized = runtime.toLowerCase();
|
|
1761
|
+
const detectedSource = String(item.detected_source || '').trim().toLowerCase();
|
|
1762
|
+
const source = sourceCandidate && sourceNormalized !== displayName && sourceNormalized !== runtimeNormalized && sourceNormalized !== detectedSource
|
|
1763
|
+
? sourceCandidate
|
|
1764
|
+
: null;
|
|
1765
|
+
const confidence = item.confidence !== 'high' ? `${item.confidence} confidence` : null;
|
|
1766
|
+
const parts = [source || null, confidence, match];
|
|
1767
|
+
const joined = joinCliSummary(parts);
|
|
1768
|
+
return joined || null;
|
|
1769
|
+
}
|
|
1440
1770
|
function printInboxGroup(label, items) {
|
|
1441
1771
|
if (items.length === 0)
|
|
1442
1772
|
return;
|
|
1443
|
-
|
|
1773
|
+
printCliSection(`${label} ${cliTag(String(items.length), getInboxGroupTone(label))}`);
|
|
1444
1774
|
for (const item of items) {
|
|
1445
|
-
|
|
1775
|
+
printCliEntry(item.display_name, {
|
|
1776
|
+
tag: item.item_type.replaceAll('_', ' '),
|
|
1777
|
+
tagTone: item.item_type === 'runtime' ? 'accent' : item.item_type === 'agent' ? 'warm' : 'muted',
|
|
1778
|
+
summary: formatSmartInboxActionLabel(item),
|
|
1779
|
+
meta: formatInboxItemMeta(item),
|
|
1780
|
+
});
|
|
1446
1781
|
}
|
|
1447
1782
|
}
|
|
1448
1783
|
function printSmartInbox(inbox) {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1784
|
+
const draftFirstCount = getDraftFirstCountFromInbox(inbox);
|
|
1785
|
+
const { privateReview, ready } = splitReadyInboxItems(inbox);
|
|
1786
|
+
const freshness = inbox.summary.freshness_state ?? 'fresh';
|
|
1787
|
+
const ageSeconds = Number.isFinite(inbox.summary.snapshot_age_seconds)
|
|
1788
|
+
? Number(inbox.summary.snapshot_age_seconds)
|
|
1789
|
+
: 0;
|
|
1790
|
+
printCliHeader('Inbox', `${formatTimestamp(inbox.summary.generated_at)} snapshot`);
|
|
1791
|
+
console.log(cliKeyLine('freshness', joinCliSummary([
|
|
1792
|
+
freshness,
|
|
1793
|
+
formatCliAge(ageSeconds),
|
|
1794
|
+
])));
|
|
1795
|
+
console.log(cliKeyLine('queue', summarizeReviewQueueCounts({
|
|
1796
|
+
draftFirstCount,
|
|
1797
|
+
readyCount: ready.length,
|
|
1798
|
+
needsReviewCount: inbox.groups.needs_confirmation.length,
|
|
1799
|
+
connectedCount: inbox.groups.connected.length,
|
|
1800
|
+
})));
|
|
1455
1801
|
if (inbox.summary.next_recommended_action) {
|
|
1456
|
-
console.log(
|
|
1802
|
+
console.log(cliKeyLine('next', formatSmartInboxActionValue(inbox.summary.next_recommended_action)));
|
|
1803
|
+
}
|
|
1804
|
+
printInboxGroup('Private review', privateReview);
|
|
1805
|
+
printInboxGroup('Needs review', inbox.groups.needs_confirmation);
|
|
1806
|
+
printInboxGroup('Ready to connect', ready);
|
|
1807
|
+
printInboxGroup('Connected', inbox.groups.connected);
|
|
1808
|
+
printInboxGroup('Denied on this device', inbox.groups.ignored);
|
|
1809
|
+
}
|
|
1810
|
+
function formatCliEvidenceSummary(confidence, sourceLabel) {
|
|
1811
|
+
const normalizedConfidence = String(confidence || '').trim().toLowerCase();
|
|
1812
|
+
if (normalizedConfidence === 'high' || normalizedConfidence === 'medium' || normalizedConfidence === 'low') {
|
|
1813
|
+
return formatCliEvidenceCell(normalizedConfidence);
|
|
1457
1814
|
}
|
|
1815
|
+
const source = String(sourceLabel || '').trim();
|
|
1816
|
+
return source ? truncateCliText(source, 14) : 'local review';
|
|
1817
|
+
}
|
|
1818
|
+
function formatCliReviewSubject(entry) {
|
|
1819
|
+
if (entry.kind === 'process') {
|
|
1820
|
+
return String(entry.process_name || 'Unknown process').trim() || 'Unknown process';
|
|
1821
|
+
}
|
|
1822
|
+
return String(entry.model_name || entry.runtime_name || 'Unknown item').trim() || 'Unknown item';
|
|
1823
|
+
}
|
|
1824
|
+
function formatCliReviewRuntime(entry) {
|
|
1825
|
+
if (entry.kind === 'provider') {
|
|
1826
|
+
return String(entry.runtime_name || 'runtime').trim() || 'runtime';
|
|
1827
|
+
}
|
|
1828
|
+
return String(entry.runtime_name || 'local').trim() || 'local';
|
|
1829
|
+
}
|
|
1830
|
+
function formatReviewSuggestedActionCompact(action) {
|
|
1831
|
+
switch (action) {
|
|
1832
|
+
case 'connect_model':
|
|
1833
|
+
return 'Connect model';
|
|
1834
|
+
case 'review_drafts':
|
|
1835
|
+
return 'Open draft';
|
|
1836
|
+
case 'already_connected':
|
|
1837
|
+
return 'Already linked';
|
|
1838
|
+
case 'review/runtime':
|
|
1839
|
+
return 'Check runtime';
|
|
1840
|
+
case 'review/connect':
|
|
1841
|
+
case 'review_model':
|
|
1842
|
+
return 'Review item';
|
|
1843
|
+
default:
|
|
1844
|
+
return truncateCliText(action.replaceAll('_', ' '), 24);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
function buildDiscoveryInteractiveSections(snapshot) {
|
|
1848
|
+
const mapRows = (groupLabel, items) => items.map((entry) => ({ groupLabel, entry }));
|
|
1849
|
+
const sections = [
|
|
1850
|
+
{ id: 'new', label: 'New', tone: 'accent', rows: mapRows('New models', snapshot.groups.new_unregistered) },
|
|
1851
|
+
{ id: 'known', label: 'Known', tone: 'warm', rows: mapRows('Known models', snapshot.groups.known_unregistered) },
|
|
1852
|
+
{ id: 'drafts', label: 'Drafts', tone: 'good', rows: mapRows('Pending drafts', snapshot.groups.pending_draft) },
|
|
1853
|
+
{ id: 'linked', label: 'Linked', tone: 'muted', rows: mapRows('Connected', snapshot.groups.bound_passport) },
|
|
1854
|
+
{ id: 'runtime', label: 'Runtime', tone: 'warm', rows: mapRows('Unconnected runtimes', snapshot.groups.shadow_candidate) },
|
|
1855
|
+
{ id: 'unavailable', label: 'Offline', tone: 'danger', rows: mapRows('Unavailable runtimes', snapshot.groups.provider_unavailable) },
|
|
1856
|
+
];
|
|
1857
|
+
return sections.filter((section) => section.rows.length > 0);
|
|
1858
|
+
}
|
|
1859
|
+
function buildDiscoveryInteractiveDetailLines(row) {
|
|
1860
|
+
const entry = row.entry;
|
|
1861
|
+
const lines = [
|
|
1862
|
+
formatCliDetailLine('Review', row.groupLabel),
|
|
1863
|
+
formatCliDetailLine('Item', formatCliReviewSubject(entry)),
|
|
1864
|
+
formatCliDetailLine('Runtime', formatCliReviewRuntime(entry)),
|
|
1865
|
+
formatCliDetailLine('Next', formatReviewSuggestedActionCompact(entry.suggested_action)),
|
|
1866
|
+
formatCliDetailLine('Evidence', formatCliEvidenceSummary(entry.confidence, entry.source_label)),
|
|
1867
|
+
];
|
|
1868
|
+
if (entry.source_label) {
|
|
1869
|
+
lines.push(formatCliDetailLine('Source', entry.source_label));
|
|
1870
|
+
}
|
|
1871
|
+
if (entry.discovery_hash) {
|
|
1872
|
+
lines.push(formatCliDetailLine('Discovery', shortId(entry.discovery_hash)));
|
|
1873
|
+
}
|
|
1874
|
+
if (entry.registration_key) {
|
|
1875
|
+
lines.push(formatCliDetailLine('Registration', shortId(entry.registration_key)));
|
|
1876
|
+
}
|
|
1877
|
+
if (entry.draft_id) {
|
|
1878
|
+
lines.push(formatCliDetailLine('Draft', shortId(entry.draft_id)));
|
|
1879
|
+
}
|
|
1880
|
+
if (entry.passport_gaid) {
|
|
1881
|
+
lines.push(formatCliDetailLine('Passport', shortId(entry.passport_gaid)));
|
|
1882
|
+
}
|
|
1883
|
+
if (entry.reason) {
|
|
1884
|
+
lines.push(formatCliDetailLine('Reason', truncateCliText(entry.reason, 64)));
|
|
1885
|
+
}
|
|
1886
|
+
return lines;
|
|
1887
|
+
}
|
|
1888
|
+
async function maybeShowInteractiveDiscoveryReviewTable(snapshot) {
|
|
1889
|
+
return await viewInteractiveTable({
|
|
1890
|
+
title: '[forkit-connect] Review',
|
|
1891
|
+
subtitle: `${formatCliCompactCount(snapshot.total, 'local item')} across models and runtimes`,
|
|
1892
|
+
sections: buildDiscoveryInteractiveSections(snapshot),
|
|
1893
|
+
columns: [
|
|
1894
|
+
{ header: 'Item', width: 30, render: (row) => formatCliReviewSubject(row.entry) },
|
|
1895
|
+
{ header: 'Runtime', width: 18, render: (row) => formatCliReviewRuntime(row.entry) },
|
|
1896
|
+
{ header: 'Next', width: 24, render: (row) => formatReviewSuggestedActionCompact(row.entry.suggested_action) },
|
|
1897
|
+
{ header: 'Evidence', width: 14, render: (row) => formatCliEvidenceSummary(row.entry.confidence, row.entry.source_label) },
|
|
1898
|
+
],
|
|
1899
|
+
detailLines: buildDiscoveryInteractiveDetailLines,
|
|
1900
|
+
detailTitle: (row) => `${row.entry.kind.toUpperCase()} SNAPSHOT`,
|
|
1901
|
+
emptyState: 'No local review items are available right now.',
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
function formatAgentSuggestedActionCompact(action) {
|
|
1905
|
+
switch (action) {
|
|
1906
|
+
case 'none':
|
|
1907
|
+
return 'Already linked';
|
|
1908
|
+
case 'rescan_agent':
|
|
1909
|
+
return 'Rescan agent';
|
|
1910
|
+
case 'review_agent':
|
|
1911
|
+
return 'Review agent';
|
|
1912
|
+
case 'link_model':
|
|
1913
|
+
return 'Link model';
|
|
1914
|
+
case 'connect_agent':
|
|
1915
|
+
return 'Connect agent';
|
|
1916
|
+
default:
|
|
1917
|
+
return truncateCliText(action.replaceAll('_', ' '), 24);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
function buildAgentInteractiveSections(snapshot) {
|
|
1921
|
+
const rows = (groupLabel, items) => items.map((entry) => ({ groupLabel, entry }));
|
|
1922
|
+
const sections = [
|
|
1923
|
+
{ id: 'new', label: 'New', tone: 'accent', rows: rows('New agents', snapshot.groups.new_agent) },
|
|
1924
|
+
{ id: 'known', label: 'Needs link', tone: 'warm', rows: rows('Needs link', snapshot.groups.known_agent) },
|
|
1925
|
+
{ id: 'linked', label: 'Linked', tone: 'good', rows: rows('Linked agents', snapshot.groups.linked_agent) },
|
|
1926
|
+
{ id: 'inactive', label: 'Inactive', tone: 'danger', rows: rows('Inactive agents', snapshot.groups.inactive_agent) },
|
|
1927
|
+
{ id: 'tooling', label: 'Tooling', tone: 'muted', rows: rows('Unknown AI tools', snapshot.groups.unknown_ai_tool) },
|
|
1928
|
+
];
|
|
1929
|
+
return sections.filter((section) => section.rows.length > 0);
|
|
1930
|
+
}
|
|
1931
|
+
function buildAgentInteractiveDetailLines(row) {
|
|
1932
|
+
const entry = row.entry;
|
|
1933
|
+
const lines = [
|
|
1934
|
+
formatCliDetailLine('Review', row.groupLabel),
|
|
1935
|
+
formatCliDetailLine('Status', entry.status.replaceAll('_', ' ')),
|
|
1936
|
+
formatCliDetailLine('Next', formatAgentSuggestedActionCompact(entry.suggested_action)),
|
|
1937
|
+
formatCliDetailLine('Evidence', formatCliEvidenceSummary(entry.confidence, entry.source_label)),
|
|
1938
|
+
formatCliDetailLine('Agent ID', shortId(entry.agent_id)),
|
|
1939
|
+
];
|
|
1940
|
+
if (entry.source_label) {
|
|
1941
|
+
lines.push(formatCliDetailLine('Source', entry.source_label));
|
|
1942
|
+
}
|
|
1943
|
+
if (entry.linked_model_name) {
|
|
1944
|
+
lines.push(formatCliDetailLine('Model', entry.linked_model_name));
|
|
1945
|
+
}
|
|
1946
|
+
if (entry.linked_passport_gaid) {
|
|
1947
|
+
lines.push(formatCliDetailLine('Passport', shortId(entry.linked_passport_gaid)));
|
|
1948
|
+
}
|
|
1949
|
+
if (entry.matched_terms?.length) {
|
|
1950
|
+
lines.push(formatCliDetailLine('Matched', truncateCliText(entry.matched_terms.join(', '), 64)));
|
|
1951
|
+
}
|
|
1952
|
+
return lines;
|
|
1953
|
+
}
|
|
1954
|
+
async function maybeShowInteractiveAgentReviewTable(snapshot) {
|
|
1955
|
+
return await viewInteractiveTable({
|
|
1956
|
+
title: '[forkit-connect] Agent Review',
|
|
1957
|
+
subtitle: `${formatCliCompactCount(snapshot.total, 'agent item')} discovered on this device`,
|
|
1958
|
+
sections: buildAgentInteractiveSections(snapshot),
|
|
1959
|
+
columns: [
|
|
1960
|
+
{ header: 'Agent', width: 28, render: (row) => row.entry.agent_name },
|
|
1961
|
+
{ header: 'Type', width: 18, render: (row) => row.entry.agent_type.replaceAll('_', ' ') },
|
|
1962
|
+
{ header: 'State', width: 16, render: (row) => row.entry.status.replaceAll('_', ' ') },
|
|
1963
|
+
{ header: 'Next', width: 22, render: (row) => formatAgentSuggestedActionCompact(row.entry.suggested_action) },
|
|
1964
|
+
],
|
|
1965
|
+
detailLines: buildAgentInteractiveDetailLines,
|
|
1966
|
+
detailTitle: () => 'AGENT SNAPSHOT',
|
|
1967
|
+
emptyState: 'No agent review items are available right now.',
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
function formatWorkspaceStateCell(workspace) {
|
|
1971
|
+
return [String(workspace.visibility || '').trim(), String(workspace.lifecycleStatus || '').trim()]
|
|
1972
|
+
.filter(Boolean)
|
|
1973
|
+
.join(' · ') || 'active';
|
|
1974
|
+
}
|
|
1975
|
+
function buildWorkspaceInteractiveDetailLines(row) {
|
|
1976
|
+
const workspace = row.workspace;
|
|
1977
|
+
const lines = [
|
|
1978
|
+
formatCliDetailLine('Workspace', String(workspace.name || 'Unnamed workspace').trim() || 'Unnamed workspace'),
|
|
1979
|
+
formatCliDetailLine('Role', String(workspace.role || 'unknown').trim() || 'unknown'),
|
|
1980
|
+
formatCliDetailLine('Visibility', String(workspace.visibility || 'not set').trim() || 'not set'),
|
|
1981
|
+
formatCliDetailLine('Lifecycle', String(workspace.lifecycleStatus || 'active').trim() || 'active'),
|
|
1982
|
+
formatCliDetailLine('Verification', String(workspace.verificationStatus || 'unverified').trim() || 'unverified'),
|
|
1983
|
+
formatCliDetailLine('ID', String(workspace.id || workspace.gaid || workspace.passportGaid || 'unknown').trim() || 'unknown'),
|
|
1984
|
+
];
|
|
1985
|
+
if (workspace.ownerName) {
|
|
1986
|
+
lines.push(formatCliDetailLine('Owner', String(workspace.ownerName).trim()));
|
|
1987
|
+
}
|
|
1988
|
+
if (workspace.description) {
|
|
1989
|
+
lines.push(formatCliDetailLine('Notes', truncateCliText(workspace.description, 64)));
|
|
1990
|
+
}
|
|
1991
|
+
return lines;
|
|
1992
|
+
}
|
|
1993
|
+
async function maybeShowInteractiveWorkspaceTable(workspaces) {
|
|
1994
|
+
return await viewInteractiveTable({
|
|
1995
|
+
title: '[forkit-connect] Workspaces',
|
|
1996
|
+
subtitle: `${formatCliCompactCount(workspaces.length, 'accessible workspace')} on this account`,
|
|
1997
|
+
sections: [{ id: 'all', label: 'Accessible', tone: 'accent', rows: workspaces.map((workspace) => ({ workspace })) }],
|
|
1998
|
+
columns: [
|
|
1999
|
+
{ header: 'Workspace', width: 28, render: (row) => String(row.workspace.name || 'Unnamed workspace').trim() || 'Unnamed workspace' },
|
|
2000
|
+
{ header: 'Role', width: 14, render: (row) => String(row.workspace.role || 'unknown').trim() || 'unknown' },
|
|
2001
|
+
{ header: 'State', width: 26, render: (row) => formatWorkspaceStateCell(row.workspace) },
|
|
2002
|
+
{ header: 'ID', width: 14, render: (row) => shortId(String(row.workspace.id || row.workspace.gaid || row.workspace.passportGaid || 'unknown')) },
|
|
2003
|
+
],
|
|
2004
|
+
detailLines: buildWorkspaceInteractiveDetailLines,
|
|
2005
|
+
detailTitle: () => 'WORKSPACE SNAPSHOT',
|
|
2006
|
+
emptyState: 'No workspaces are available right now.',
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
function buildProjectInteractiveDetailLines(row) {
|
|
2010
|
+
const project = row.project;
|
|
2011
|
+
const passportCount = Number(project.passportCount);
|
|
2012
|
+
const lines = [
|
|
2013
|
+
formatCliDetailLine('Project', String(project.name || 'Unnamed project').trim() || 'Unnamed project'),
|
|
2014
|
+
formatCliDetailLine('Status', String(project.status || 'active').trim() || 'active'),
|
|
2015
|
+
formatCliDetailLine('Passports', Number.isFinite(passportCount) && passportCount >= 0 ? String(passportCount) : 'n/a'),
|
|
2016
|
+
formatCliDetailLine('Project ID', String(project.id || 'unknown').trim() || 'unknown'),
|
|
2017
|
+
formatCliDetailLine('Workspace', shortId(row.workspaceId || String(project.workspaceId || ''))),
|
|
2018
|
+
];
|
|
2019
|
+
if (project.updatedAt) {
|
|
2020
|
+
lines.push(formatCliDetailLine('Updated', formatTimestamp(project.updatedAt)));
|
|
2021
|
+
}
|
|
2022
|
+
if (project.description) {
|
|
2023
|
+
lines.push(formatCliDetailLine('Notes', truncateCliText(project.description, 64)));
|
|
2024
|
+
}
|
|
2025
|
+
return lines;
|
|
2026
|
+
}
|
|
2027
|
+
async function maybeShowInteractiveProjectTable(projects, workspaceId) {
|
|
2028
|
+
return await viewInteractiveTable({
|
|
2029
|
+
title: '[forkit-connect] Projects',
|
|
2030
|
+
subtitle: `${formatCliCompactCount(projects.length, 'project')} inside workspace ${shortId(workspaceId)}`,
|
|
2031
|
+
sections: [{ id: 'all', label: 'Available', tone: 'accent', rows: projects.map((project) => ({ workspaceId, project })) }],
|
|
2032
|
+
columns: [
|
|
2033
|
+
{ header: 'Project', width: 28, render: (row) => String(row.project.name || 'Unnamed project').trim() || 'Unnamed project' },
|
|
2034
|
+
{ header: 'Status', width: 16, render: (row) => String(row.project.status || 'active').trim() || 'active' },
|
|
2035
|
+
{ header: 'Passports', width: 12, render: (row) => {
|
|
2036
|
+
const passportCount = Number(row.project.passportCount);
|
|
2037
|
+
return Number.isFinite(passportCount) && passportCount >= 0 ? String(passportCount) : '-';
|
|
2038
|
+
}, align: 'right' },
|
|
2039
|
+
{ header: 'Project ID', width: 14, render: (row) => shortId(String(row.project.id || 'unknown')) },
|
|
2040
|
+
],
|
|
2041
|
+
detailLines: buildProjectInteractiveDetailLines,
|
|
2042
|
+
detailTitle: () => 'PROJECT SNAPSHOT',
|
|
2043
|
+
emptyState: 'No projects are available right now.',
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
function buildInboxInteractiveSections(inbox) {
|
|
2047
|
+
const { privateReview, ready } = splitReadyInboxItems(inbox);
|
|
2048
|
+
const rows = (groupLabel, items) => (items.map((item) => ({ groupLabel, item })));
|
|
2049
|
+
const sections = [
|
|
2050
|
+
{ id: 'review', label: 'Review', tone: 'warm', rows: rows('Needs review', inbox.groups.needs_confirmation) },
|
|
2051
|
+
{ id: 'private', label: 'Private', tone: 'accent', rows: rows('Private review', privateReview) },
|
|
2052
|
+
{ id: 'ready', label: 'Ready', tone: 'good', rows: rows('Ready to connect', ready) },
|
|
2053
|
+
{ id: 'connected', label: 'Connected', tone: 'muted', rows: rows('Connected', inbox.groups.connected) },
|
|
2054
|
+
{ id: 'denied', label: 'Denied', tone: 'danger', rows: rows('Denied on this device', inbox.groups.ignored) },
|
|
2055
|
+
];
|
|
2056
|
+
return sections.filter((section) => section.rows.length > 0);
|
|
2057
|
+
}
|
|
2058
|
+
function buildInboxInteractiveDetailLines(row) {
|
|
2059
|
+
const item = row.item;
|
|
2060
|
+
const lines = [
|
|
2061
|
+
formatCliDetailLine('Review', row.groupLabel),
|
|
2062
|
+
formatCliDetailLine('Next', formatSmartInboxActionLabel(item)),
|
|
2063
|
+
formatCliDetailLine('Evidence', `${formatCliEvidenceLabel(item.confidence)} local evidence`),
|
|
2064
|
+
formatCliDetailLine('Source', item.detected_source),
|
|
2065
|
+
];
|
|
2066
|
+
if (item.workspaceId || item.projectId) {
|
|
2067
|
+
lines.push(formatCliDetailLine('Workspace', formatScopeDetailValue(item.workspaceId, 'workspace')));
|
|
2068
|
+
lines.push(formatCliDetailLine('Project', formatScopeDetailValue(item.projectId, 'project')));
|
|
2069
|
+
}
|
|
2070
|
+
else {
|
|
2071
|
+
lines.push(formatCliDetailLine('Scope', 'solo / no workspace'));
|
|
2072
|
+
}
|
|
2073
|
+
if (item.matched_passport_gaid) {
|
|
2074
|
+
lines.push(formatCliDetailLine('Passport', item.matched_passport_gaid));
|
|
2075
|
+
}
|
|
2076
|
+
return lines;
|
|
2077
|
+
}
|
|
2078
|
+
async function maybeShowInteractiveInboxTable(inbox) {
|
|
2079
|
+
const freshness = inbox.summary.freshness_state ?? 'fresh';
|
|
2080
|
+
const ageSeconds = Number.isFinite(inbox.summary.snapshot_age_seconds)
|
|
2081
|
+
? Number(inbox.summary.snapshot_age_seconds)
|
|
2082
|
+
: 0;
|
|
2083
|
+
return await viewInteractiveTable({
|
|
2084
|
+
title: '[forkit-connect] Inbox',
|
|
2085
|
+
subtitle: joinCliSummary([
|
|
2086
|
+
`${formatTimestamp(inbox.summary.generated_at)} snapshot`,
|
|
2087
|
+
freshness,
|
|
2088
|
+
formatCliAge(ageSeconds),
|
|
2089
|
+
]),
|
|
2090
|
+
sections: buildInboxInteractiveSections(inbox),
|
|
2091
|
+
columns: [
|
|
2092
|
+
{ header: 'Item', width: 32, render: (row) => row.item.display_name },
|
|
2093
|
+
{ header: 'Type', width: 18, render: (row) => row.item.item_type.replaceAll('_', ' ') },
|
|
2094
|
+
{ header: 'Next', width: 28, render: (row) => formatSmartInboxActionLabel(row.item) },
|
|
2095
|
+
{ header: 'Evidence', width: 14, render: (row) => formatCliEvidenceCell(row.item.confidence) },
|
|
2096
|
+
],
|
|
2097
|
+
detailLines: buildInboxInteractiveDetailLines,
|
|
2098
|
+
detailTitle: (row) => `${row.item.item_type.replaceAll('_', ' ').toUpperCase()} SNAPSHOT`,
|
|
2099
|
+
emptyState: 'No inbox items are available right now.',
|
|
2100
|
+
});
|
|
1458
2101
|
}
|
|
1459
2102
|
function printConnectStatusOverview(status) {
|
|
1460
2103
|
const sessionMissing = !status.device_paired && status.binding_state === 'active' && !status.credential_reconnect_needed;
|
|
@@ -1483,20 +2126,28 @@ function printConnectStatusOverview(status) {
|
|
|
1483
2126
|
}
|
|
1484
2127
|
function printPublicStatusOverview(status) {
|
|
1485
2128
|
const otherReadyCount = getOtherReadyCount(status.ready_to_connect_count, status.draft_first_count);
|
|
1486
|
-
|
|
1487
|
-
console.log(
|
|
1488
|
-
console.log(
|
|
1489
|
-
console.log(
|
|
1490
|
-
console.log(
|
|
1491
|
-
console.log(
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
2129
|
+
printCliHeader('Status');
|
|
2130
|
+
console.log(cliKeyLine('device', status.device_paired ? 'paired' : 'approval pending'));
|
|
2131
|
+
console.log(cliKeyLine('scope', formatCliScopeLabel(status.workspace_id, status.project_id)));
|
|
2132
|
+
console.log(cliKeyLine('daemon', status.daemon_status));
|
|
2133
|
+
console.log(cliKeyLine('privacy', status.privacy_mode));
|
|
2134
|
+
console.log(cliKeyLine('inventory', joinCliSummary([
|
|
2135
|
+
formatCliCompactCount(status.models_discovered, 'model'),
|
|
2136
|
+
formatCliCompactCount(status.agents_discovered, 'agent'),
|
|
2137
|
+
formatCliCompactCount(status.runtimes_discovered, 'runtime'),
|
|
2138
|
+
])));
|
|
2139
|
+
console.log(cliKeyLine('queue', summarizeReviewQueueCounts({
|
|
2140
|
+
draftFirstCount: status.draft_first_count,
|
|
2141
|
+
readyCount: otherReadyCount,
|
|
2142
|
+
needsReviewCount: status.needs_confirmation_count,
|
|
2143
|
+
})));
|
|
2144
|
+
console.log(cliKeyLine('linked', formatCliCompactCount(status.connected_count, 'connected record')));
|
|
2145
|
+
console.log(cliKeyLine('sync', `${status.c2_sync_pending} pending runtime events`));
|
|
1495
2146
|
if (status.lifecycle_note) {
|
|
1496
|
-
console.log(
|
|
2147
|
+
console.log(cliKeyLine('governed', truncateCliText(status.lifecycle_note, 88)));
|
|
1497
2148
|
}
|
|
1498
2149
|
if (!status.device_paired && status.binding_state === 'active') {
|
|
1499
|
-
console.log('
|
|
2150
|
+
console.log(cliKeyLine('warning', 'Local governed scope exists, but login is still required.'));
|
|
1500
2151
|
}
|
|
1501
2152
|
}
|
|
1502
2153
|
function getOtherReadyCount(readyToConnectCount, draftFirstCount) {
|
|
@@ -1510,34 +2161,65 @@ function printPublicStatusGuidance(status, sessionState) {
|
|
|
1510
2161
|
: sessionState === 'unavailable'
|
|
1511
2162
|
? 'unverified'
|
|
1512
2163
|
: 'login required';
|
|
1513
|
-
console.log(
|
|
2164
|
+
console.log(cliKeyLine('account', accountLabel));
|
|
1514
2165
|
if (sessionState === 'missing') {
|
|
1515
|
-
console.log('
|
|
1516
|
-
console.log('
|
|
2166
|
+
console.log(cliKeyLine('session', 'Local discovery is working. Sign in to pair this device.'));
|
|
2167
|
+
console.log(cliKeyLine('next', 'forkit-connect login'));
|
|
1517
2168
|
if (status.binding_state === 'active' && status.workspace_id && status.project_id) {
|
|
1518
|
-
console.log('
|
|
2169
|
+
console.log(cliKeyLine('scope', 'This device still has saved governed scope from an older session.'));
|
|
1519
2170
|
}
|
|
1520
2171
|
return;
|
|
1521
2172
|
}
|
|
1522
2173
|
if (sessionState === 'expired') {
|
|
1523
|
-
console.log('
|
|
1524
|
-
console.log('
|
|
2174
|
+
console.log(cliKeyLine('session', 'The local device is ready, but the account session needs renewal.'));
|
|
2175
|
+
console.log(cliKeyLine('next', 'forkit-connect login'));
|
|
1525
2176
|
return;
|
|
1526
2177
|
}
|
|
1527
2178
|
if (!status.device_paired) {
|
|
1528
|
-
console.log('
|
|
1529
|
-
console.log('
|
|
2179
|
+
console.log(cliKeyLine('session', 'Local identity is ready, but browser approval still needs to finish.'));
|
|
2180
|
+
console.log(cliKeyLine('next', 'Finish approval in the browser, then rerun forkit-connect status'));
|
|
1530
2181
|
return;
|
|
1531
2182
|
}
|
|
1532
2183
|
if (status.daemon_status === 'stopped') {
|
|
1533
|
-
console.log('
|
|
1534
|
-
console.log('
|
|
2184
|
+
console.log(cliKeyLine('daemon', 'Background sync is idle until you start the local daemon.'));
|
|
2185
|
+
console.log(cliKeyLine('next', 'forkit-connect start'));
|
|
1535
2186
|
return;
|
|
1536
2187
|
}
|
|
1537
2188
|
if (status.next_recommended_action) {
|
|
1538
|
-
console.log(
|
|
2189
|
+
console.log(cliKeyLine('next', formatSmartInboxActionValue(status.next_recommended_action)));
|
|
1539
2190
|
}
|
|
1540
2191
|
}
|
|
2192
|
+
function buildRuntimeInteractiveDetailLines(runtime) {
|
|
2193
|
+
return [
|
|
2194
|
+
formatCliDetailLine('State', runtime.status.replaceAll('_', ' ')),
|
|
2195
|
+
formatCliDetailLine('Health', runtime.health_status),
|
|
2196
|
+
formatCliDetailLine('Endpoint', runtime.source_endpoint_label || runtime.runtime_type),
|
|
2197
|
+
formatCliDetailLine('Models', runtime.linked_models_count),
|
|
2198
|
+
formatCliDetailLine('Agents', runtime.linked_agents_count),
|
|
2199
|
+
formatCliDetailLine('Next', formatRuntimeRecommendedActionLabel(runtime.recommended_action)),
|
|
2200
|
+
];
|
|
2201
|
+
}
|
|
2202
|
+
async function maybeShowInteractiveRuntimeReviewTable(summary) {
|
|
2203
|
+
const healthy = summary.runtimes.filter((runtime) => runtime.status !== 'unavailable' && runtime.health_status === 'detected');
|
|
2204
|
+
const review = summary.runtimes.filter((runtime) => !healthy.includes(runtime));
|
|
2205
|
+
return await viewInteractiveTable({
|
|
2206
|
+
title: '[forkit-connect] Runtime Review',
|
|
2207
|
+
subtitle: `${formatCliCompactCount(summary.total_runtimes, 'runtime')} on this device`,
|
|
2208
|
+
sections: [
|
|
2209
|
+
{ id: 'healthy', label: 'Healthy', tone: 'good', rows: healthy },
|
|
2210
|
+
{ id: 'review', label: 'Needs review', tone: 'warm', rows: review },
|
|
2211
|
+
],
|
|
2212
|
+
columns: [
|
|
2213
|
+
{ header: 'Runtime', width: 24, render: (runtime) => runtime.runtime_name },
|
|
2214
|
+
{ header: 'State', width: 18, render: (runtime) => runtime.status.replaceAll('_', ' ') },
|
|
2215
|
+
{ header: 'Health', width: 12, render: (runtime) => runtime.health_status },
|
|
2216
|
+
{ header: 'Next', width: 24, render: (runtime) => formatRuntimeRecommendedActionLabel(runtime.recommended_action) },
|
|
2217
|
+
],
|
|
2218
|
+
detailLines: buildRuntimeInteractiveDetailLines,
|
|
2219
|
+
detailTitle: () => 'RUNTIME SNAPSHOT',
|
|
2220
|
+
emptyState: 'No runtimes are available right now.',
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
1541
2223
|
async function checkBackendSessionState(service) {
|
|
1542
2224
|
const sessionRefValue = String(service.readSessionRef() || '').trim();
|
|
1543
2225
|
if (!sessionRefValue)
|
|
@@ -1676,6 +2358,180 @@ async function promptSelection(label, options) {
|
|
|
1676
2358
|
node_process_1.stdin.on('keypress', onKeypress);
|
|
1677
2359
|
});
|
|
1678
2360
|
}
|
|
2361
|
+
function paintCliTone(text, tone) {
|
|
2362
|
+
switch (tone) {
|
|
2363
|
+
case 'accent':
|
|
2364
|
+
return cliAccent(text);
|
|
2365
|
+
case 'good':
|
|
2366
|
+
return cliGood(text);
|
|
2367
|
+
case 'warm':
|
|
2368
|
+
return cliWarm(text);
|
|
2369
|
+
case 'danger':
|
|
2370
|
+
return cliDanger(text);
|
|
2371
|
+
default:
|
|
2372
|
+
return cliDim(text);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
function fitCliTableCell(value, width, align = 'left') {
|
|
2376
|
+
const rendered = truncateCliText(value, width);
|
|
2377
|
+
const pad = Math.max(0, width - cliPlainWidth(rendered));
|
|
2378
|
+
return align === 'right'
|
|
2379
|
+
? `${' '.repeat(pad)}${rendered}`
|
|
2380
|
+
: `${rendered}${' '.repeat(pad)}`;
|
|
2381
|
+
}
|
|
2382
|
+
function buildCliTableBorder(widths, left, middle, right) {
|
|
2383
|
+
return `${left}${widths.map((width) => '─'.repeat(width + 2)).join(middle)}${right}`;
|
|
2384
|
+
}
|
|
2385
|
+
function buildCliTableRow(cells) {
|
|
2386
|
+
return `│${cells.map((cell) => ` ${fitCliTableCell(cell.value, cell.width, cell.align)} `).join('│')}│`;
|
|
2387
|
+
}
|
|
2388
|
+
function buildCliWideBox(title, lines, width) {
|
|
2389
|
+
const innerWidth = Math.max(24, width);
|
|
2390
|
+
const safeTitle = truncateCliText(title, Math.max(8, innerWidth - 4));
|
|
2391
|
+
const topPad = Math.max(0, innerWidth - cliPlainWidth(safeTitle) - 2);
|
|
2392
|
+
const top = `┌─ ${safeTitle} ${'─'.repeat(topPad)}┐`;
|
|
2393
|
+
const body = lines.map((line) => `│ ${fitCliTableCell(line, innerWidth)} │`);
|
|
2394
|
+
const bottom = `└${'─'.repeat(innerWidth + 2)}┘`;
|
|
2395
|
+
return [top, ...body, bottom];
|
|
2396
|
+
}
|
|
2397
|
+
function cliTableCell(value, width, align) {
|
|
2398
|
+
return align ? { value, width, align } : { value, width };
|
|
2399
|
+
}
|
|
2400
|
+
function formatCliTableTabs(sections, activeIndex) {
|
|
2401
|
+
return sections
|
|
2402
|
+
.map((section, index) => {
|
|
2403
|
+
const label = `${section.label} ${section.rows.length}`;
|
|
2404
|
+
if (index === activeIndex) {
|
|
2405
|
+
return paintCli(` ${label} `, '7');
|
|
2406
|
+
}
|
|
2407
|
+
return paintCliTone(`[${label}]`, section.tone ?? 'muted');
|
|
2408
|
+
})
|
|
2409
|
+
.join(' ');
|
|
2410
|
+
}
|
|
2411
|
+
async function viewInteractiveTable(options) {
|
|
2412
|
+
const sections = options.sections.filter((section) => section.rows.length > 0);
|
|
2413
|
+
if (!sections.length || !node_process_1.stdin.isTTY || !node_process_1.stdout.isTTY || hasFlag('--plain')) {
|
|
2414
|
+
return false;
|
|
2415
|
+
}
|
|
2416
|
+
(0, node_readline_1.emitKeypressEvents)(node_process_1.stdin);
|
|
2417
|
+
const canUseRawMode = typeof node_process_1.stdin.setRawMode === 'function';
|
|
2418
|
+
const previousRawMode = canUseRawMode ? node_process_1.stdin.isRaw : false;
|
|
2419
|
+
if (canUseRawMode) {
|
|
2420
|
+
node_process_1.stdin.setRawMode(true);
|
|
2421
|
+
}
|
|
2422
|
+
let activeSectionIndex = 0;
|
|
2423
|
+
let selectedIndex = 0;
|
|
2424
|
+
let renderLineCount = 0;
|
|
2425
|
+
const visibleRows = 8;
|
|
2426
|
+
const reset = '\u001B[0m';
|
|
2427
|
+
const hideCursor = '\u001B[?25l';
|
|
2428
|
+
const showCursor = '\u001B[?25h';
|
|
2429
|
+
const inverse = '\u001B[7m';
|
|
2430
|
+
const render = () => {
|
|
2431
|
+
const activeSection = sections[activeSectionIndex] ?? sections[0];
|
|
2432
|
+
const rows = activeSection?.rows ?? [];
|
|
2433
|
+
const currentRow = rows[selectedIndex] ?? null;
|
|
2434
|
+
const startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(visibleRows / 2), Math.max(0, rows.length - visibleRows)));
|
|
2435
|
+
const visibleSlice = rows.slice(startIndex, startIndex + visibleRows);
|
|
2436
|
+
const selectorWidth = 1;
|
|
2437
|
+
const columnWidths = [selectorWidth, ...options.columns.map((column) => column.width)];
|
|
2438
|
+
const headerLine = buildCliTableRow([
|
|
2439
|
+
cliTableCell(' ', selectorWidth),
|
|
2440
|
+
...options.columns.map((column) => cliTableCell(column.header.toUpperCase(), column.width, column.align)),
|
|
2441
|
+
]);
|
|
2442
|
+
const rowLines = visibleSlice.length > 0
|
|
2443
|
+
? visibleSlice.map((row, offset) => {
|
|
2444
|
+
const absoluteIndex = startIndex + offset;
|
|
2445
|
+
const active = absoluteIndex === selectedIndex;
|
|
2446
|
+
const line = buildCliTableRow([
|
|
2447
|
+
cliTableCell(active ? cliAccent('›') : ' ', selectorWidth),
|
|
2448
|
+
...options.columns.map((column, index) => cliTableCell(active && index === 0 ? cliBold(column.render(row)) : column.render(row), column.width, column.align)),
|
|
2449
|
+
]);
|
|
2450
|
+
return active ? cliAccent(line) : line;
|
|
2451
|
+
})
|
|
2452
|
+
: buildCliWideBox('EMPTY', [options.emptyState], columnWidths.reduce((sum, value) => sum + value + 3, -1));
|
|
2453
|
+
const detail = currentRow ? options.detailLines(currentRow) : [options.emptyState];
|
|
2454
|
+
const innerTableWidth = columnWidths.reduce((sum, value) => sum + value + 3, -1);
|
|
2455
|
+
const tableTop = buildCliTableBorder(columnWidths, '┌', '┬', '┐');
|
|
2456
|
+
const tableMiddle = buildCliTableBorder(columnWidths, '├', '┼', '┤');
|
|
2457
|
+
const tableBottom = buildCliTableBorder(columnWidths, '└', '┴', '┘');
|
|
2458
|
+
const detailTitle = currentRow && options.detailTitle ? options.detailTitle(currentRow) : 'SELECTED ITEM';
|
|
2459
|
+
const detailBox = buildCliWideBox(detailTitle, detail, innerTableWidth);
|
|
2460
|
+
const lines = [
|
|
2461
|
+
cliBold(options.title),
|
|
2462
|
+
...(options.subtitle ? [cliDim(options.subtitle)] : []),
|
|
2463
|
+
cliRule(),
|
|
2464
|
+
formatCliTableTabs(sections, activeSectionIndex),
|
|
2465
|
+
'',
|
|
2466
|
+
cliDim(tableTop),
|
|
2467
|
+
cliDim(headerLine),
|
|
2468
|
+
cliDim(tableMiddle),
|
|
2469
|
+
...rowLines,
|
|
2470
|
+
cliDim(tableBottom),
|
|
2471
|
+
'',
|
|
2472
|
+
...detailBox.map((line) => cliDim(line)),
|
|
2473
|
+
'',
|
|
2474
|
+
cliDim('Tab switch lane · Up/Down move · q or Esc exit'),
|
|
2475
|
+
];
|
|
2476
|
+
if (renderLineCount > 0) {
|
|
2477
|
+
node_process_1.stdout.write(`\u001B[${renderLineCount}A`);
|
|
2478
|
+
node_process_1.stdout.write('\u001B[0J');
|
|
2479
|
+
}
|
|
2480
|
+
node_process_1.stdout.write(`${hideCursor}${lines.join('\n')}\n`);
|
|
2481
|
+
renderLineCount = lines.length;
|
|
2482
|
+
};
|
|
2483
|
+
const clampSelection = () => {
|
|
2484
|
+
const activeRows = sections[activeSectionIndex]?.rows ?? [];
|
|
2485
|
+
selectedIndex = Math.max(0, Math.min(selectedIndex, Math.max(activeRows.length - 1, 0)));
|
|
2486
|
+
};
|
|
2487
|
+
render();
|
|
2488
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
2489
|
+
await new Promise((resolve) => {
|
|
2490
|
+
const cleanup = () => {
|
|
2491
|
+
node_process_1.stdin.off('keypress', onKeypress);
|
|
2492
|
+
if (canUseRawMode) {
|
|
2493
|
+
node_process_1.stdin.setRawMode(previousRawMode === true);
|
|
2494
|
+
}
|
|
2495
|
+
node_process_1.stdout.write(`\u001B[${renderLineCount}A`);
|
|
2496
|
+
node_process_1.stdout.write(`\u001B[0J${showCursor}`);
|
|
2497
|
+
resolve();
|
|
2498
|
+
};
|
|
2499
|
+
const onKeypress = (_value, key) => {
|
|
2500
|
+
if (key.ctrl && key.name === 'c') {
|
|
2501
|
+
cleanup();
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
2505
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
2506
|
+
render();
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
2510
|
+
const rows = sections[activeSectionIndex]?.rows ?? [];
|
|
2511
|
+
selectedIndex = Math.min(Math.max(rows.length - 1, 0), selectedIndex + 1);
|
|
2512
|
+
render();
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
if (key.name === 'tab' || key.name === 'right' || key.name === 'l') {
|
|
2516
|
+
activeSectionIndex = (activeSectionIndex + 1) % sections.length;
|
|
2517
|
+
clampSelection();
|
|
2518
|
+
render();
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
if (key.name === 'left' || key.name === 'h') {
|
|
2522
|
+
activeSectionIndex = (activeSectionIndex - 1 + sections.length) % sections.length;
|
|
2523
|
+
clampSelection();
|
|
2524
|
+
render();
|
|
2525
|
+
return;
|
|
2526
|
+
}
|
|
2527
|
+
if (key.name === 'escape' || key.name === 'q' || key.name === 'return' || key.name === 'enter') {
|
|
2528
|
+
cleanup();
|
|
2529
|
+
}
|
|
2530
|
+
};
|
|
2531
|
+
node_process_1.stdin.on('keypress', onKeypress);
|
|
2532
|
+
});
|
|
2533
|
+
return true;
|
|
2534
|
+
}
|
|
1679
2535
|
async function promptText(question, options) {
|
|
1680
2536
|
const rl = (0, promises_1.createInterface)({ input: node_process_1.stdin, output: node_process_1.stdout });
|
|
1681
2537
|
try {
|
|
@@ -1810,9 +2666,9 @@ async function run() {
|
|
|
1810
2666
|
return;
|
|
1811
2667
|
}
|
|
1812
2668
|
printPublicStatusOverview(displayOverview);
|
|
1813
|
-
console.log(
|
|
2669
|
+
console.log(cliKeyLine('secure', secureStorage.backend));
|
|
1814
2670
|
if (!secureStorage.available || secureStorage.plaintextFallbackActive) {
|
|
1815
|
-
console.log(
|
|
2671
|
+
console.log(cliKeyLine('secure note', secureStorage.detail));
|
|
1816
2672
|
}
|
|
1817
2673
|
printPublicStatusGuidance(displayOverview, sessionState);
|
|
1818
2674
|
};
|
|
@@ -1828,17 +2684,16 @@ async function run() {
|
|
|
1828
2684
|
return;
|
|
1829
2685
|
}
|
|
1830
2686
|
const freshness = inbox.summary.freshness_state ?? 'fresh';
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2687
|
+
if (await maybeShowInteractiveInboxTable(inbox)) {
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
printSmartInbox(inbox);
|
|
1835
2691
|
if (freshness === 'stale') {
|
|
1836
|
-
console.log('
|
|
2692
|
+
console.log(cliKeyLine('refresh', 'Run forkit-connect inbox --refresh for an immediate reconcile.'));
|
|
1837
2693
|
}
|
|
1838
2694
|
else if (freshness === 'syncing') {
|
|
1839
|
-
console.log('
|
|
2695
|
+
console.log(cliKeyLine('refresh', 'Background reconcile is still running.'));
|
|
1840
2696
|
}
|
|
1841
|
-
printSmartInbox(inbox);
|
|
1842
2697
|
};
|
|
1843
2698
|
const runPublicCollectedChanges = () => {
|
|
1844
2699
|
const rawLimit = limitArg !== null ? Number(limitArg) : 20;
|
|
@@ -4113,16 +4968,13 @@ async function run() {
|
|
|
4113
4968
|
return;
|
|
4114
4969
|
}
|
|
4115
4970
|
const accountLimits = await loadCliAccountLimits().catch(() => null);
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
console.log(`- welcome=${accountLimits.displayName || 'there'}`);
|
|
4119
|
-
console.log(`- plan=${accountLimits.planName}`);
|
|
4120
|
-
console.log(`- workspaces_left=${formatRemainingLimit(accountLimits.workspaceLimit, accountLimits.workspacesUsed, 'workspace')}`);
|
|
4121
|
-
console.log(`- projects_left=${formatRemainingLimit(accountLimits.projectLimit, accountLimits.projectsUsed, 'project')}`);
|
|
4122
|
-
}
|
|
4123
|
-
for (const workspace of workspaces) {
|
|
4124
|
-
console.log(formatWorkspaceAccessLine(workspace));
|
|
4971
|
+
if (await maybeShowInteractiveWorkspaceTable(workspaces)) {
|
|
4972
|
+
return;
|
|
4125
4973
|
}
|
|
4974
|
+
printWorkspaceListSurface(workspaces, {
|
|
4975
|
+
displayName: accountLimits?.displayName || null,
|
|
4976
|
+
accountLimits,
|
|
4977
|
+
});
|
|
4126
4978
|
return;
|
|
4127
4979
|
}
|
|
4128
4980
|
if (subcommand === 'select') {
|
|
@@ -4173,6 +5025,9 @@ async function run() {
|
|
|
4173
5025
|
printJson(summary);
|
|
4174
5026
|
return;
|
|
4175
5027
|
}
|
|
5028
|
+
if (await maybeShowInteractiveRuntimeReviewTable(summary)) {
|
|
5029
|
+
return;
|
|
5030
|
+
}
|
|
4176
5031
|
printRuntimeReview(summary);
|
|
4177
5032
|
return;
|
|
4178
5033
|
}
|
|
@@ -4426,10 +5281,17 @@ async function run() {
|
|
|
4426
5281
|
if (command === 'review') {
|
|
4427
5282
|
const reviewScan = await service.scanRuntime();
|
|
4428
5283
|
const snapshot = service.buildReviewSnapshot(reviewScan.summary);
|
|
5284
|
+
if (hasFlag('--json')) {
|
|
5285
|
+
printJson(snapshot);
|
|
5286
|
+
return;
|
|
5287
|
+
}
|
|
4429
5288
|
if (snapshot.total === 0) {
|
|
4430
5289
|
console.log('No review items found. Run `forkit-connect scan` first or ensure local runtimes are available.');
|
|
4431
5290
|
return;
|
|
4432
5291
|
}
|
|
5292
|
+
if (await maybeShowInteractiveDiscoveryReviewTable(snapshot)) {
|
|
5293
|
+
return;
|
|
5294
|
+
}
|
|
4433
5295
|
printReviewSnapshot(snapshot);
|
|
4434
5296
|
return;
|
|
4435
5297
|
}
|
|
@@ -4480,6 +5342,9 @@ async function run() {
|
|
|
4480
5342
|
printJson(summary);
|
|
4481
5343
|
return;
|
|
4482
5344
|
}
|
|
5345
|
+
if (await maybeShowInteractiveRuntimeReviewTable(summary)) {
|
|
5346
|
+
return;
|
|
5347
|
+
}
|
|
4483
5348
|
printRuntimeReview(summary);
|
|
4484
5349
|
return;
|
|
4485
5350
|
}
|
|
@@ -4920,10 +5785,17 @@ async function run() {
|
|
|
4920
5785
|
}
|
|
4921
5786
|
if (subcommand === 'review') {
|
|
4922
5787
|
const snapshot = service.buildAgentReviewSnapshot();
|
|
5788
|
+
if (hasFlag('--json')) {
|
|
5789
|
+
printJson(snapshot);
|
|
5790
|
+
return;
|
|
5791
|
+
}
|
|
4923
5792
|
if (snapshot.total === 0) {
|
|
4924
5793
|
console.log('No agent items found. Run `forkit-connect agent scan` first.');
|
|
4925
5794
|
return;
|
|
4926
5795
|
}
|
|
5796
|
+
if (await maybeShowInteractiveAgentReviewTable(snapshot)) {
|
|
5797
|
+
return;
|
|
5798
|
+
}
|
|
4927
5799
|
printAgentReview(snapshot);
|
|
4928
5800
|
return;
|
|
4929
5801
|
}
|
|
@@ -5319,22 +6191,14 @@ async function run() {
|
|
|
5319
6191
|
: typeof tokenPayload?.role === 'string'
|
|
5320
6192
|
? tokenPayload.role
|
|
5321
6193
|
: null;
|
|
5322
|
-
if (email || platformRole) {
|
|
5323
|
-
const identityParts = [
|
|
5324
|
-
email ? `account=${email}` : null,
|
|
5325
|
-
platformRole ? `platform_role=${platformRole}` : null,
|
|
5326
|
-
].filter(Boolean);
|
|
5327
|
-
console.log(`[forkit-connect] ${identityParts.join(' | ')}`);
|
|
5328
|
-
}
|
|
5329
6194
|
const workspaces = Array.isArray(payload.workspaces) ? payload.workspaces : [];
|
|
5330
|
-
|
|
5331
|
-
if (workspaces.length === 0) {
|
|
5332
|
-
console.log('[forkit-connect] No accessible workspaces returned by /api/profiles/access.');
|
|
6195
|
+
if (await maybeShowInteractiveWorkspaceTable(workspaces)) {
|
|
5333
6196
|
return;
|
|
5334
6197
|
}
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
6198
|
+
printWorkspaceListSurface(workspaces, {
|
|
6199
|
+
accountEmail: email,
|
|
6200
|
+
platformRole,
|
|
6201
|
+
});
|
|
5338
6202
|
return;
|
|
5339
6203
|
}
|
|
5340
6204
|
if (command === 'projects') {
|
|
@@ -5373,17 +6237,11 @@ async function run() {
|
|
|
5373
6237
|
process.exitCode = 2;
|
|
5374
6238
|
return;
|
|
5375
6239
|
}
|
|
5376
|
-
console.log('Authenticated successfully.');
|
|
5377
6240
|
const projects = Array.isArray(result.body.projects) ? result.body.projects : [];
|
|
5378
|
-
|
|
5379
|
-
if (projects.length === 0) {
|
|
5380
|
-
console.log('No projects found in this workspace.');
|
|
5381
|
-
console.log('Run `forkit-connect workspace select --workspace <id> --project-name "<name>"` to create the first project here.');
|
|
6241
|
+
if (await maybeShowInteractiveProjectTable(projects, workspaceId)) {
|
|
5382
6242
|
return;
|
|
5383
6243
|
}
|
|
5384
|
-
|
|
5385
|
-
console.log(formatWorkspaceProjectLine(project));
|
|
5386
|
-
}
|
|
6244
|
+
printProjectListSurface(projects, workspaceId);
|
|
5387
6245
|
return;
|
|
5388
6246
|
}
|
|
5389
6247
|
if (command === 'bind') {
|