@phnx-labs/agents-cli 1.20.92 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +385 -0
- package/README.md +3 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +3 -5
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/events.js +91 -1
- package/dist/commands/exec.js +13 -0
- package/dist/commands/mailboxes.js +39 -1
- package/dist/commands/message.js +12 -1
- package/dist/commands/packages.js +14 -5
- package/dist/commands/projects.d.ts +38 -0
- package/dist/commands/projects.js +355 -48
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +198 -7
- package/dist/commands/send.d.ts +14 -12
- package/dist/commands/send.js +105 -35
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +16 -8
- package/dist/commands/setup-browser.js +5 -1
- package/dist/commands/setup-fleet.js +5 -0
- package/dist/commands/setup-preferences.d.ts +53 -0
- package/dist/commands/setup-preferences.js +142 -0
- package/dist/commands/setup.js +8 -9
- package/dist/commands/ssh.js +220 -10
- package/dist/commands/sync.d.ts +2 -2
- package/dist/commands/sync.js +13 -12
- package/dist/commands/view.js +4 -0
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +38 -6
- package/dist/lib/activity.d.ts +15 -0
- package/dist/lib/activity.js +26 -2
- package/dist/lib/auto-dispatch.d.ts +6 -1
- package/dist/lib/auto-dispatch.js +7 -2
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/channels/send.d.ts +83 -0
- package/dist/lib/channels/send.js +112 -0
- package/dist/lib/daemon.d.ts +13 -0
- package/dist/lib/daemon.js +80 -60
- package/dist/lib/device-config.d.ts +82 -0
- package/dist/lib/device-config.js +296 -0
- package/dist/lib/event-provenance.d.ts +19 -0
- package/dist/lib/event-provenance.js +48 -0
- package/dist/lib/events-ingest.d.ts +46 -0
- package/dist/lib/events-ingest.js +182 -0
- package/dist/lib/events.d.ts +17 -5
- package/dist/lib/events.js +59 -56
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +2 -0
- package/dist/lib/git.d.ts +13 -2
- package/dist/lib/git.js +38 -6
- package/dist/lib/linear-project-counts.d.ts +129 -0
- package/dist/lib/linear-project-counts.js +206 -0
- package/dist/lib/linear-projects.d.ts +62 -0
- package/dist/lib/linear-projects.js +144 -0
- package/dist/lib/mailbox-gc.js +30 -7
- package/dist/lib/mailbox.d.ts +14 -1
- package/dist/lib/mailbox.js +35 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +5 -21
- package/dist/lib/menubar/install-menubar.js +35 -44
- package/dist/lib/menubar/notify-desktop.d.ts +17 -2
- package/dist/lib/menubar/notify-desktop.js +8 -2
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/models.js +21 -11
- package/dist/lib/overdue.d.ts +1 -1
- package/dist/lib/overdue.js +54 -19
- package/dist/lib/project-import.d.ts +96 -0
- package/dist/lib/project-import.js +171 -0
- package/dist/lib/project-probe.d.ts +75 -0
- package/dist/lib/project-probe.js +160 -0
- package/dist/lib/project-resources.d.ts +8 -0
- package/dist/lib/project-resources.js +31 -3
- package/dist/lib/project-status.d.ts +32 -1
- package/dist/lib/project-status.js +82 -1
- package/dist/lib/projects.d.ts +6 -0
- package/dist/lib/projects.js +12 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routine-notify.d.ts +11 -0
- package/dist/lib/routine-notify.js +22 -0
- package/dist/lib/routines.d.ts +53 -0
- package/dist/lib/routines.js +105 -4
- package/dist/lib/rules/run-sync.d.ts +18 -0
- package/dist/lib/rules/run-sync.js +92 -0
- package/dist/lib/run-notify.js +3 -0
- package/dist/lib/scheduler.js +12 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +53 -10
- package/dist/lib/secrets/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/secrets/list-filter.d.ts +20 -5
- package/dist/lib/secrets/list-filter.js +22 -6
- package/dist/lib/secrets/usage-db.d.ts +106 -0
- package/dist/lib/secrets/usage-db.js +236 -0
- package/dist/lib/session/active.d.ts +16 -2
- package/dist/lib/session/active.js +8 -4
- package/dist/lib/session/bundle.js +5 -1
- package/dist/lib/session/remote-active.d.ts +5 -1
- package/dist/lib/session/remote-active.js +4 -1
- package/dist/lib/session/remote-list.d.ts +1 -15
- package/dist/lib/session/remote-list.js +22 -111
- package/dist/lib/session/sync/config.d.ts +13 -5
- package/dist/lib/session/sync/config.js +21 -13
- package/dist/lib/sqlite.js +28 -1
- package/dist/lib/startup/command-registry.d.ts +0 -2
- package/dist/lib/startup/command-registry.js +1 -5
- package/dist/lib/state.d.ts +19 -2
- package/dist/lib/state.js +99 -10
- package/dist/lib/sync-umbrella.d.ts +11 -23
- package/dist/lib/sync-umbrella.js +14 -45
- package/dist/lib/teams/agents.d.ts +35 -3
- package/dist/lib/teams/agents.js +52 -8
- package/dist/lib/teams/scheduler.d.ts +29 -2
- package/dist/lib/teams/scheduler.js +60 -16
- package/dist/lib/types.d.ts +22 -5
- package/dist/lib/versions.d.ts +6 -0
- package/dist/lib/versions.js +6 -4
- package/package.json +1 -2
- package/dist/commands/drive.d.ts +0 -10
- package/dist/commands/drive.js +0 -183
- package/dist/commands/hq.d.ts +0 -2
- package/dist/commands/hq.js +0 -58
- package/dist/commands/sessions-sync.d.ts +0 -17
- package/dist/commands/sessions-sync.js +0 -135
- package/dist/commands/sync-provision.d.ts +0 -23
- package/dist/commands/sync-provision.js +0 -107
- package/dist/lib/drive-sync.d.ts +0 -45
- package/dist/lib/drive-sync.js +0 -238
- package/dist/lib/hq/floor.d.ts +0 -87
- package/dist/lib/hq/floor.js +0 -243
- package/dist/lib/session/sync/crdt.d.ts +0 -44
- package/dist/lib/session/sync/crdt.js +0 -119
- package/dist/lib/session/sync/manifest.d.ts +0 -62
- package/dist/lib/session/sync/manifest.js +0 -100
- package/dist/lib/session/sync/provision.d.ts +0 -49
- package/dist/lib/session/sync/provision.js +0 -91
- package/dist/lib/session/sync/r2.d.ts +0 -32
- package/dist/lib/session/sync/r2.js +0 -121
- package/dist/lib/session/sync/sync.d.ts +0 -106
- package/dist/lib/session/sync/sync.js +0 -374
package/dist/lib/models.js
CHANGED
|
@@ -20,7 +20,14 @@ const CACHE_PATH = getModelsCachePath();
|
|
|
20
20
|
* Bump when the extractor logic changes shape in an incompatible way so cached
|
|
21
21
|
* catalogs from older agents-cli builds are re-extracted.
|
|
22
22
|
*/
|
|
23
|
-
const CACHE_SCHEMA_VERSION =
|
|
23
|
+
const CACHE_SCHEMA_VERSION = 3;
|
|
24
|
+
/**
|
|
25
|
+
* How long a cached 0-model extraction is trusted before we retry it. Bounds
|
|
26
|
+
* the self-healing window for a transient failure (mid-install, a broken
|
|
27
|
+
* extractor regex fixed in a later agents-cli release) without falling back
|
|
28
|
+
* to re-extracting -- and re-scanning the whole binary -- on every call.
|
|
29
|
+
*/
|
|
30
|
+
const EMPTY_CATALOG_RETRY_MS = 24 * 60 * 60 * 1000;
|
|
24
31
|
let memoryCache = null;
|
|
25
32
|
function cacheKey(agent, version) {
|
|
26
33
|
return `${agent}@${version}`;
|
|
@@ -884,7 +891,10 @@ export function getModelCatalog(agent, version) {
|
|
|
884
891
|
const key = cacheKey(agent, version);
|
|
885
892
|
const cached = cache.entries[key];
|
|
886
893
|
if (cached && cached.sourcePath === src.path && cached.mtime === mtime) {
|
|
887
|
-
|
|
894
|
+
const isFresh = cached.catalog.models.length > 0 ||
|
|
895
|
+
Date.now() - (cached.attemptedAt ?? 0) < EMPTY_CATALOG_RETRY_MS;
|
|
896
|
+
if (isFresh)
|
|
897
|
+
return cached.catalog;
|
|
888
898
|
}
|
|
889
899
|
let models = [];
|
|
890
900
|
let aliases = {};
|
|
@@ -927,15 +937,15 @@ export function getModelCatalog(agent, version) {
|
|
|
927
937
|
models,
|
|
928
938
|
aliases,
|
|
929
939
|
};
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
940
|
+
// Cache a 0-model extraction too, stamped with when it was attempted, so a
|
|
941
|
+
// broken/mid-install extractor doesn't force a full re-scan of the source
|
|
942
|
+
// binary (up to ~1.85s each for a 230-270MB Claude binary) on every call --
|
|
943
|
+
// `getModelCatalog` runs once per installed version per invocation of
|
|
944
|
+
// commands like `agents view`. It self-heals: the read site above re-tries
|
|
945
|
+
// extraction once EMPTY_CATALOG_RETRY_MS has elapsed, or immediately once
|
|
946
|
+
// the source file's mtime changes (an upgrade/reinstall).
|
|
947
|
+
cache.entries[key] = { sourcePath: src.path, mtime, catalog, attemptedAt: Date.now() };
|
|
948
|
+
saveCache();
|
|
939
949
|
return catalog;
|
|
940
950
|
}
|
|
941
951
|
/**
|
package/dist/lib/overdue.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface OverdueJob {
|
|
|
31
31
|
* Returns null when neither is available, which leaves the routine unfloored
|
|
32
32
|
* (previous behaviour) rather than silently skipping it.
|
|
33
33
|
*/
|
|
34
|
-
export declare function routineEffectiveStart(job: JobConfig): Date | null;
|
|
34
|
+
export declare function routineEffectiveStart(job: JobConfig, now?: Date): Date | null;
|
|
35
35
|
/** Return every enabled, recurring job whose most recent expected fire was
|
|
36
36
|
* missed. One-shot jobs are excluded — they fire at most once. */
|
|
37
37
|
export declare function detectOverdueJobs(now?: Date): OverdueJob[];
|
package/dist/lib/overdue.js
CHANGED
|
@@ -13,29 +13,48 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import { Cron } from 'croner';
|
|
16
|
-
import { listJobs, getLatestRun,
|
|
16
|
+
import { listJobs, getLatestRun, resolveJobFilePath, isPastEndAt, isOneShotRoutine, jobRunsOnThisDevice } from './routines.js';
|
|
17
17
|
import { notifyDesktop } from './menubar/notify-desktop.js';
|
|
18
18
|
// Tolerance between "expected fire" and "recorded run start" — accounts for
|
|
19
19
|
// the small gap between the cron tick and when the runner writes meta.json.
|
|
20
20
|
const GRACE_MS = 60_000;
|
|
21
|
-
const
|
|
21
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
22
|
+
/**
|
|
23
|
+
* Lookback windows, narrowest first. A fixed one-week window silently blinded
|
|
24
|
+
* detection to any cron whose gap exceeds it: `0 9 1,13,25 * *` has 12-day gaps,
|
|
25
|
+
* so `nextRun(now - 7d)` jumped past `now`, the walk returned null, and the
|
|
26
|
+
* routine was never flagged overdue on any device — no missed record, no
|
|
27
|
+
* catch-up, permanently. Monthly, quarterly and annual routines were all in that
|
|
28
|
+
* class.
|
|
29
|
+
*
|
|
30
|
+
* A wider window is only tried when the narrower one found nothing, so a dense
|
|
31
|
+
* schedule (every minute, hourly, daily) never walks more than a week of
|
|
32
|
+
* occurrences. A sparse schedule has few occurrences to walk by definition.
|
|
33
|
+
*/
|
|
34
|
+
const LOOKBACK_WINDOWS_MS = [7 * DAY_MS, 32 * DAY_MS, 93 * DAY_MS, 400 * DAY_MS];
|
|
22
35
|
/** Compute the most recent fire of `pattern` at or before `now`. Croner's
|
|
23
36
|
* `previousRun()` returns the cron instance's own last fire, which is null
|
|
24
37
|
* on a freshly-constructed instance — so we walk `nextRun(cursor)` forward
|
|
25
38
|
* from a week ago and keep the last fire still ≤ now. */
|
|
26
39
|
function previousExpectedFire(cron, now) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
for (const window of LOOKBACK_WINDOWS_MS) {
|
|
41
|
+
let cursor = new Date(now.getTime() - window);
|
|
42
|
+
let last = null;
|
|
43
|
+
// Cap iterations: an every-minute schedule yields ≤ 10080 steps over a week;
|
|
44
|
+
// 20k is a paranoia bound against pathological patterns. Only a schedule
|
|
45
|
+
// that found nothing in the narrower window reaches a wider one, and such a
|
|
46
|
+
// schedule is sparse, so the cap is never the binding constraint.
|
|
47
|
+
for (let i = 0; i < 20000; i++) {
|
|
48
|
+
const next = cron.nextRun(cursor);
|
|
49
|
+
if (!next || next.getTime() > now.getTime())
|
|
50
|
+
break;
|
|
51
|
+
last = next;
|
|
52
|
+
cursor = next;
|
|
53
|
+
}
|
|
54
|
+
if (last)
|
|
55
|
+
return last;
|
|
37
56
|
}
|
|
38
|
-
return
|
|
57
|
+
return null;
|
|
39
58
|
}
|
|
40
59
|
/**
|
|
41
60
|
* When a routine started existing, and therefore the earliest fire it can
|
|
@@ -49,13 +68,17 @@ function previousExpectedFire(cron, now) {
|
|
|
49
68
|
* Returns null when neither is available, which leaves the routine unfloored
|
|
50
69
|
* (previous behaviour) rather than silently skipping it.
|
|
51
70
|
*/
|
|
52
|
-
export function routineEffectiveStart(job) {
|
|
71
|
+
export function routineEffectiveStart(job, now = new Date()) {
|
|
53
72
|
if (job.createdAt) {
|
|
54
73
|
const stamped = new Date(job.createdAt);
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
// Clamp a future stamp (clock skew, a hand-edited year) to now. Left
|
|
75
|
+
// unclamped it sits after every possible expected fire, so the routine can
|
|
76
|
+
// never be flagged overdue until wall-clock time catches up.
|
|
77
|
+
if (!isNaN(stamped.getTime())) {
|
|
78
|
+
return stamped.getTime() > now.getTime() ? now : stamped;
|
|
79
|
+
}
|
|
57
80
|
}
|
|
58
|
-
const path =
|
|
81
|
+
const path = resolveJobFilePath(job.name);
|
|
59
82
|
if (!path)
|
|
60
83
|
return null;
|
|
61
84
|
try {
|
|
@@ -70,7 +93,19 @@ export function routineEffectiveStart(job) {
|
|
|
70
93
|
export function detectOverdueJobs(now = new Date()) {
|
|
71
94
|
const overdue = [];
|
|
72
95
|
for (const job of listJobs()) {
|
|
73
|
-
if (!job.enabled
|
|
96
|
+
if (!job.enabled)
|
|
97
|
+
continue;
|
|
98
|
+
// One-shot: fires at most once, so a missed slot is not a backlog to replay.
|
|
99
|
+
// Use the same predicate the scheduler does — the raw `runOnce` flag alone
|
|
100
|
+
// missed a one-shot-LIKE schedule (a fixed minute/hour/day/month) that never
|
|
101
|
+
// carried the flag.
|
|
102
|
+
if (isOneShotRoutine(job))
|
|
103
|
+
continue;
|
|
104
|
+
// Past its configured end: catch-up must not resurrect a routine the author
|
|
105
|
+
// already retired. The scheduler only auto-disables lazily, inside a live
|
|
106
|
+
// cron tick, so a routine whose endAt elapsed while the daemon was down is
|
|
107
|
+
// still enabled on disk when the catch-up pass runs.
|
|
108
|
+
if (isPastEndAt(job, now))
|
|
74
109
|
continue;
|
|
75
110
|
// Trigger-only jobs (no cron schedule) never have an expected fire time.
|
|
76
111
|
if (!job.schedule)
|
|
@@ -99,7 +134,7 @@ export function detectOverdueJobs(now = new Date()) {
|
|
|
99
134
|
// a miss. Without this, any newly created routine on a daily/weekly cron is
|
|
100
135
|
// instantly "overdue" for the previous occurrence — and with auto-catchup
|
|
101
136
|
// that means `agents routines add` runs the routine once, immediately.
|
|
102
|
-
const start = routineEffectiveStart(job);
|
|
137
|
+
const start = routineEffectiveStart(job, now);
|
|
103
138
|
if (start && expected.getTime() < start.getTime())
|
|
104
139
|
continue;
|
|
105
140
|
const latest = getLatestRun(job.name);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure builders behind `agents projects import`.
|
|
3
|
+
*
|
|
4
|
+
* Two sources feed the one `ProjectDef` schema: **Linear** (the preferred one —
|
|
5
|
+
* a project someone deliberately created on the board) and the **Factory
|
|
6
|
+
* registry** (`~/.agents/factory/projects.json`, an auto-detection that guesses
|
|
7
|
+
* from checkouts on disk). Both funnel through `writeProjectDef`.
|
|
8
|
+
*
|
|
9
|
+
* The Factory registry stamps each row with a `confidence` — and importing every
|
|
10
|
+
* row regardless is what buried the real projects under a dozen guesses
|
|
11
|
+
* (`agents-cleaned-stale2`, a repo cloned from someone else's org, …). So the
|
|
12
|
+
* import is gated on that field, `high` by default.
|
|
13
|
+
*
|
|
14
|
+
* Everything here is a pure function of its arguments — no fs, no shell, no
|
|
15
|
+
* process env. The command layer reads the registry / shells out to `linear` and
|
|
16
|
+
* hands the rows in; that's what makes these testable against plain fixtures
|
|
17
|
+
* with no mocking. The one exception is `toHomeRelative`, a string rewrite
|
|
18
|
+
* against `$HOME`.
|
|
19
|
+
*/
|
|
20
|
+
import { type ProjectDef } from './projects.js';
|
|
21
|
+
import { type LinearProjectLite } from './linear-projects.js';
|
|
22
|
+
/** Factory's per-row detection confidence, weakest first. */
|
|
23
|
+
export type ImportConfidence = 'low' | 'medium' | 'high';
|
|
24
|
+
/**
|
|
25
|
+
* The floor an import runs at. `any` is what `--all` means — it takes rows that
|
|
26
|
+
* state no confidence at all, which rank below even `low`; without it "import
|
|
27
|
+
* every row regardless of confidence" would quietly drop the unranked ones.
|
|
28
|
+
*/
|
|
29
|
+
export type ImportFloor = ImportConfidence | 'any';
|
|
30
|
+
/** A row the import declined to write, with the reason to print. */
|
|
31
|
+
export interface ImportSkip {
|
|
32
|
+
name: string;
|
|
33
|
+
reason: string;
|
|
34
|
+
}
|
|
35
|
+
/** What an import would write, and what it declined. */
|
|
36
|
+
export interface ImportPlan {
|
|
37
|
+
defs: ProjectDef[];
|
|
38
|
+
skipped: ImportSkip[];
|
|
39
|
+
}
|
|
40
|
+
/** The validated shape of the `import` flags. */
|
|
41
|
+
export interface ImportOptions {
|
|
42
|
+
source: 'factory' | 'linear';
|
|
43
|
+
/** Factory only: the lowest confidence that still imports. */
|
|
44
|
+
minConfidence: ImportFloor;
|
|
45
|
+
force: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** The raw commander flags, before validation. */
|
|
48
|
+
export interface RawImportFlags {
|
|
49
|
+
fromFactory?: boolean;
|
|
50
|
+
fromLinear?: boolean;
|
|
51
|
+
minConfidence?: string;
|
|
52
|
+
all?: boolean;
|
|
53
|
+
force?: boolean;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Validate the flag combination, throwing a user-facing message on the first
|
|
57
|
+
* problem. Every rejection is loud: an unrecognized `--min-confidence` is an
|
|
58
|
+
* error, never a silent fall back to the default floor.
|
|
59
|
+
*/
|
|
60
|
+
export declare function validateImportOpts(flags: RawImportFlags): ImportOptions;
|
|
61
|
+
/**
|
|
62
|
+
* Plan the Factory import: same field mapping as before (`path`→`root`,
|
|
63
|
+
* `repoSlug`→`repo`, `linearProjectId`→`linear.projectId`), now gated on the
|
|
64
|
+
* row's `confidence`. A row with no confidence field is a guess with no stated
|
|
65
|
+
* strength, so it ranks below every floor and only `--all` takes it.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildFactoryImportCandidates(rows: unknown[], existing: Map<string, ProjectDef>, opts: Pick<ImportOptions, 'minConfidence' | 'force'>): ImportPlan;
|
|
68
|
+
/**
|
|
69
|
+
* Turn a Linear project name into a definition slug: lowercase, every run of
|
|
70
|
+
* unusable characters collapsed to one `-`, trimmed of leading/trailing
|
|
71
|
+
* punctuation, capped at the 64 chars `isSafeProjectName` allows. Returns `''`
|
|
72
|
+
* when nothing usable survives — the caller skips those loudly.
|
|
73
|
+
*/
|
|
74
|
+
export declare function slugifyProjectName(name: string): string;
|
|
75
|
+
/** The local-checkout lookups the Linear builder needs, injected so it stays pure. */
|
|
76
|
+
export interface LinearImportDeps {
|
|
77
|
+
/** Directory names directly under the configured projects root. */
|
|
78
|
+
localDirs: string[];
|
|
79
|
+
/** Home-relative root path for one of `localDirs`. */
|
|
80
|
+
resolveRoot: (dir: string) => string | undefined;
|
|
81
|
+
/** `owner/repo` from that checkout's origin remote, when it has one. */
|
|
82
|
+
resolveOrigin: (dir: string) => string | undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Plan the Linear import. Every project becomes a def carrying its `linear`
|
|
86
|
+
* link; the local checkout is bound **only on an exact normalized-name match**
|
|
87
|
+
* (`matchLocalCheckoutExact`). The containment fallback that powers the `link`
|
|
88
|
+
* suggestion is deliberately not used here — "Agents CLI" containing
|
|
89
|
+
* "agents-cli-web" is a fine hint for a human to confirm, and a silently wrong
|
|
90
|
+
* `root` on a write path.
|
|
91
|
+
*
|
|
92
|
+
* An existing def is preserved field-for-field; only `name` and `linear` are
|
|
93
|
+
* overwritten, so a hand-set `description`/`contexts`/`integrations` survives a
|
|
94
|
+
* re-import.
|
|
95
|
+
*/
|
|
96
|
+
export declare function buildLinearImportCandidates(projects: LinearProjectLite[], existing: Map<string, ProjectDef>, deps: LinearImportDeps, opts: Pick<ImportOptions, 'force'>): ImportPlan;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure builders behind `agents projects import`.
|
|
3
|
+
*
|
|
4
|
+
* Two sources feed the one `ProjectDef` schema: **Linear** (the preferred one —
|
|
5
|
+
* a project someone deliberately created on the board) and the **Factory
|
|
6
|
+
* registry** (`~/.agents/factory/projects.json`, an auto-detection that guesses
|
|
7
|
+
* from checkouts on disk). Both funnel through `writeProjectDef`.
|
|
8
|
+
*
|
|
9
|
+
* The Factory registry stamps each row with a `confidence` — and importing every
|
|
10
|
+
* row regardless is what buried the real projects under a dozen guesses
|
|
11
|
+
* (`agents-cleaned-stale2`, a repo cloned from someone else's org, …). So the
|
|
12
|
+
* import is gated on that field, `high` by default.
|
|
13
|
+
*
|
|
14
|
+
* Everything here is a pure function of its arguments — no fs, no shell, no
|
|
15
|
+
* process env. The command layer reads the registry / shells out to `linear` and
|
|
16
|
+
* hands the rows in; that's what makes these testable against plain fixtures
|
|
17
|
+
* with no mocking. The one exception is `toHomeRelative`, a string rewrite
|
|
18
|
+
* against `$HOME`.
|
|
19
|
+
*/
|
|
20
|
+
import { toHomeRelative } from './project-root.js';
|
|
21
|
+
import { isSafeProjectName } from './projects.js';
|
|
22
|
+
import { matchLocalCheckoutExact } from './linear-projects.js';
|
|
23
|
+
const CONFIDENCE_RANK = { any: 0, low: 1, medium: 2, high: 3 };
|
|
24
|
+
/** Rank a raw `confidence` value; anything absent or unrecognized ranks 0 (below every floor). */
|
|
25
|
+
function confidenceRank(raw) {
|
|
26
|
+
return typeof raw === 'string' ? (CONFIDENCE_RANK[raw] ?? 0) : 0;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validate the flag combination, throwing a user-facing message on the first
|
|
30
|
+
* problem. Every rejection is loud: an unrecognized `--min-confidence` is an
|
|
31
|
+
* error, never a silent fall back to the default floor.
|
|
32
|
+
*/
|
|
33
|
+
export function validateImportOpts(flags) {
|
|
34
|
+
const sources = [flags.fromFactory && 'factory', flags.fromLinear && 'linear'].filter(Boolean);
|
|
35
|
+
if (sources.length === 0)
|
|
36
|
+
throw new Error('Pick an import source: --from-linear or --from-factory.');
|
|
37
|
+
if (sources.length > 1)
|
|
38
|
+
throw new Error('--from-linear and --from-factory are mutually exclusive — pick one.');
|
|
39
|
+
const source = sources[0];
|
|
40
|
+
if (source === 'linear' && (flags.all || flags.minConfidence !== undefined)) {
|
|
41
|
+
throw new Error('--all and --min-confidence apply to --from-factory only (Linear rows carry no confidence).');
|
|
42
|
+
}
|
|
43
|
+
if (flags.all && flags.minConfidence !== undefined) {
|
|
44
|
+
throw new Error('--all and --min-confidence are mutually exclusive (--all means --min-confidence low).');
|
|
45
|
+
}
|
|
46
|
+
let minConfidence = 'high';
|
|
47
|
+
if (flags.all)
|
|
48
|
+
minConfidence = 'any';
|
|
49
|
+
else if (flags.minConfidence !== undefined) {
|
|
50
|
+
const v = flags.minConfidence.trim().toLowerCase();
|
|
51
|
+
if (v !== 'low' && v !== 'medium' && v !== 'high') {
|
|
52
|
+
throw new Error(`Invalid --min-confidence "${flags.minConfidence}" (expected low, medium, or high).`);
|
|
53
|
+
}
|
|
54
|
+
minConfidence = v;
|
|
55
|
+
}
|
|
56
|
+
return { source, minConfidence, force: flags.force === true };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Plan the Factory import: same field mapping as before (`path`→`root`,
|
|
60
|
+
* `repoSlug`→`repo`, `linearProjectId`→`linear.projectId`), now gated on the
|
|
61
|
+
* row's `confidence`. A row with no confidence field is a guess with no stated
|
|
62
|
+
* strength, so it ranks below every floor and only `--all` takes it.
|
|
63
|
+
*/
|
|
64
|
+
export function buildFactoryImportCandidates(rows, existing, opts) {
|
|
65
|
+
const floor = CONFIDENCE_RANK[opts.minConfidence];
|
|
66
|
+
const defs = [];
|
|
67
|
+
const skipped = [];
|
|
68
|
+
// The registry keys rows by `owner/repo` but names them by basename, so two
|
|
69
|
+
// repos with the same basename in different orgs (grinich/inflow, me/inflow)
|
|
70
|
+
// arrive as two rows called `inflow`. Without this the second silently
|
|
71
|
+
// overwrote the first on disk and the run still reported both as imported.
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
for (const raw of rows) {
|
|
74
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
75
|
+
continue;
|
|
76
|
+
const o = raw;
|
|
77
|
+
const name = typeof o.name === 'string' ? o.name : undefined;
|
|
78
|
+
if (!name || !isSafeProjectName(name)) {
|
|
79
|
+
skipped.push({ name: name || '(unnamed)', reason: 'not a usable project name' });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const rank = confidenceRank(o.confidence);
|
|
83
|
+
if (rank < floor) {
|
|
84
|
+
const stated = typeof o.confidence === 'string' && o.confidence ? `confidence "${o.confidence}"` : 'no confidence field';
|
|
85
|
+
skipped.push({ name, reason: `${stated} is below the "${opts.minConfidence}" floor` });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (seen.has(name)) {
|
|
89
|
+
skipped.push({ name, reason: 'another row in this registry already claimed the name' });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (existing.has(name) && !opts.force) {
|
|
93
|
+
skipped.push({ name, reason: 'already defined — pass --force to overwrite' });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
seen.add(name);
|
|
97
|
+
const def = { name };
|
|
98
|
+
if (typeof o.path === 'string')
|
|
99
|
+
def.root = toHomeRelative(o.path);
|
|
100
|
+
if (typeof o.repoSlug === 'string')
|
|
101
|
+
def.repo = o.repoSlug;
|
|
102
|
+
if (typeof o.linearProjectId === 'string')
|
|
103
|
+
def.linear = { projectId: o.linearProjectId };
|
|
104
|
+
defs.push(def);
|
|
105
|
+
}
|
|
106
|
+
return { defs, skipped };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Turn a Linear project name into a definition slug: lowercase, every run of
|
|
110
|
+
* unusable characters collapsed to one `-`, trimmed of leading/trailing
|
|
111
|
+
* punctuation, capped at the 64 chars `isSafeProjectName` allows. Returns `''`
|
|
112
|
+
* when nothing usable survives — the caller skips those loudly.
|
|
113
|
+
*/
|
|
114
|
+
export function slugifyProjectName(name) {
|
|
115
|
+
const slug = name
|
|
116
|
+
.toLowerCase()
|
|
117
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
118
|
+
.replace(/-{2,}/g, '-')
|
|
119
|
+
.replace(/^[-._]+|[-._]+$/g, '')
|
|
120
|
+
.slice(0, 64)
|
|
121
|
+
.replace(/[-._]+$/g, '');
|
|
122
|
+
return isSafeProjectName(slug) ? slug : '';
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Plan the Linear import. Every project becomes a def carrying its `linear`
|
|
126
|
+
* link; the local checkout is bound **only on an exact normalized-name match**
|
|
127
|
+
* (`matchLocalCheckoutExact`). The containment fallback that powers the `link`
|
|
128
|
+
* suggestion is deliberately not used here — "Agents CLI" containing
|
|
129
|
+
* "agents-cli-web" is a fine hint for a human to confirm, and a silently wrong
|
|
130
|
+
* `root` on a write path.
|
|
131
|
+
*
|
|
132
|
+
* An existing def is preserved field-for-field; only `name` and `linear` are
|
|
133
|
+
* overwritten, so a hand-set `description`/`contexts`/`integrations` survives a
|
|
134
|
+
* re-import.
|
|
135
|
+
*/
|
|
136
|
+
export function buildLinearImportCandidates(projects, existing, deps, opts) {
|
|
137
|
+
const defs = [];
|
|
138
|
+
const skipped = [];
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
for (const p of projects) {
|
|
141
|
+
const name = slugifyProjectName(p.name);
|
|
142
|
+
if (!name) {
|
|
143
|
+
skipped.push({ name: p.name, reason: 'no usable project name (letters, digits, ., _, - only)' });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (seen.has(name)) {
|
|
147
|
+
skipped.push({ name: p.name, reason: `another Linear project already claimed the name "${name}"` });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const prior = existing.get(name);
|
|
151
|
+
if (prior && (prior.root || prior.repo) && !opts.force) {
|
|
152
|
+
skipped.push({ name, reason: 'existing def already has root/repo — pass --force to relink' });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
seen.add(name);
|
|
156
|
+
const def = { ...prior, name, linear: { projectId: p.id } };
|
|
157
|
+
if (p.url)
|
|
158
|
+
def.linear.url = p.url;
|
|
159
|
+
const dir = matchLocalCheckoutExact(p.name, deps.localDirs);
|
|
160
|
+
if (dir) {
|
|
161
|
+
const root = deps.resolveRoot(dir);
|
|
162
|
+
if (root)
|
|
163
|
+
def.root = root;
|
|
164
|
+
const repo = deps.resolveOrigin(dir);
|
|
165
|
+
if (repo)
|
|
166
|
+
def.repo = repo;
|
|
167
|
+
}
|
|
168
|
+
defs.push(def);
|
|
169
|
+
}
|
|
170
|
+
return { defs, skipped };
|
|
171
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project workspace probing — the drift signal behind `projects status --fleet`.
|
|
3
|
+
*
|
|
4
|
+
* Projects are natively multi-device: the same definition (home-relative paths)
|
|
5
|
+
* re-roots on every fleet machine, and the question is whether the project's
|
|
6
|
+
* repos are PRESENT on each box, on which branch, how far ahead/behind their
|
|
7
|
+
* upstream, and whether they carry uncommitted changes. This module is the pure
|
|
8
|
+
* local half: given a set of home-relative paths it probes each one with a
|
|
9
|
+
* handful of read-only git calls. Drift is measured against the LAST-FETCHED
|
|
10
|
+
* upstream (`@{upstream}`) — deliberately no `git fetch`, so a probe is fast
|
|
11
|
+
* and offline-safe. The fleet half (`--fleet`) just runs this probe on every
|
|
12
|
+
* peer via the canonical `remote-agents-json` SSH fan-out.
|
|
13
|
+
*/
|
|
14
|
+
import type { ProjectDef } from './projects.js';
|
|
15
|
+
/** The on-disk state of one workspace repo on one machine. */
|
|
16
|
+
export interface RepoWorkspaceStatus {
|
|
17
|
+
/** The probed path, echoed home-relative (re-roots per machine). */
|
|
18
|
+
path: string;
|
|
19
|
+
/** `.git` exists (a directory, or a FILE for a linked worktree). */
|
|
20
|
+
present: boolean;
|
|
21
|
+
branch?: string;
|
|
22
|
+
/** The configured upstream ref (e.g. `origin/main`); absent → no upstream. */
|
|
23
|
+
upstream?: string;
|
|
24
|
+
/** Commits on HEAD not on the upstream. Undefined without an upstream. */
|
|
25
|
+
ahead?: number;
|
|
26
|
+
/** Commits on the upstream not on HEAD. Undefined without an upstream. */
|
|
27
|
+
behind?: number;
|
|
28
|
+
/** Uncommitted (incl. untracked) paths from `git status --porcelain`. */
|
|
29
|
+
dirty?: number;
|
|
30
|
+
/** ISO 8601 committer date of HEAD. */
|
|
31
|
+
lastCommit?: string;
|
|
32
|
+
/** `.git` exists but git could not read it — never looks silently clean. */
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
/** A probe result tagged with the machine that answered (the fleet view). */
|
|
36
|
+
export interface HostWorkspaceStatus extends RepoWorkspaceStatus {
|
|
37
|
+
host: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Probe one workspace repo. A missing path yields `{present: false}` and no
|
|
41
|
+
* git call is made. On a present repo every signal is best-effort: whatever
|
|
42
|
+
* succeeded is reported, and a repo whose `.git` exists yet every git call
|
|
43
|
+
* failed surfaces as present-with-error rather than silently clean.
|
|
44
|
+
*/
|
|
45
|
+
export declare function probeRepoWorkspace(absPath: string): RepoWorkspaceStatus;
|
|
46
|
+
/** Probe each home-relative path (expanded against the local home), in order. */
|
|
47
|
+
export declare function probeProjectWorkspaces(paths: string[]): RepoWorkspaceStatus[];
|
|
48
|
+
/**
|
|
49
|
+
* The home-relative paths to probe for a project definition: its `root` plus
|
|
50
|
+
* each `repos[].path` (the opt-in for additional repos), deduped. Every target
|
|
51
|
+
* is normalized through the same `toHomeRelative(expandLocalHome(...))` the
|
|
52
|
+
* probe echoes, so a hand-edited def (absolute path under home, trailing
|
|
53
|
+
* slash) matches its probe rows exactly — `writeProjectDef` normalizes on
|
|
54
|
+
* write, but defs are hand-editable YAML and never silently drop a row.
|
|
55
|
+
*/
|
|
56
|
+
export declare function workspaceTargetsForDef(def: ProjectDef): string[];
|
|
57
|
+
/**
|
|
58
|
+
* Parse a peer's `projects probe --json` stdout, tagging each row with the
|
|
59
|
+
* machine that answered. Defensive against version skew / partial output, the
|
|
60
|
+
* same boundary contract as `parseRemoteActive`: non-JSON or a non-array
|
|
61
|
+
* yields `[]`, and rows without a `path`/`present` core are dropped.
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseRemoteProbe(stdout: string, machine: string): HostWorkspaceStatus[];
|
|
64
|
+
/**
|
|
65
|
+
* One workspace's compact state: `✓ clean · main`, `⚠ 12 dirty · ↑3 ↓1 ·
|
|
66
|
+
* feature/x`, `✗ missing`, or `⚠ error: …`. Pure — chalk styling only.
|
|
67
|
+
*/
|
|
68
|
+
export declare function formatWorkspaceLine(s: RepoWorkspaceStatus): string;
|
|
69
|
+
/**
|
|
70
|
+
* The fleet view of one project's workspaces: one content line per probed
|
|
71
|
+
* path (host-sorted `host: state` cells joined by ` · `), labelled with the
|
|
72
|
+
* path when a project probes more than one. Pure — the caller adds the
|
|
73
|
+
* `fleet` row label.
|
|
74
|
+
*/
|
|
75
|
+
export declare function formatFleetWorkspaces(statuses: HostWorkspaceStatus[]): string[];
|