swarmdo 1.37.0 → 1.45.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/README.md +7 -5
- package/package.json +1 -1
- package/v3/@swarmdo/cli/dist/src/apply/apply.js +6 -1
- package/v3/@swarmdo/cli/dist/src/changelog/changelog.d.ts +21 -0
- package/v3/@swarmdo/cli/dist/src/changelog/changelog.js +43 -0
- package/v3/@swarmdo/cli/dist/src/commands/changelog.js +15 -4
- package/v3/@swarmdo/cli/dist/src/commands/config.js +6 -1
- package/v3/@swarmdo/cli/dist/src/commands/coupling.d.ts +17 -0
- package/v3/@swarmdo/cli/dist/src/commands/coupling.js +85 -0
- package/v3/@swarmdo/cli/dist/src/commands/hooks.js +55 -0
- package/v3/@swarmdo/cli/dist/src/commands/hotspots.js +2 -1
- package/v3/@swarmdo/cli/dist/src/commands/index.js +6 -3
- package/v3/@swarmdo/cli/dist/src/commands/redact.js +11 -0
- package/v3/@swarmdo/cli/dist/src/commands/release.js +27 -5
- package/v3/@swarmdo/cli/dist/src/commands/task.js +57 -2
- package/v3/@swarmdo/cli/dist/src/config-lint/agents-lint.d.ts +30 -0
- package/v3/@swarmdo/cli/dist/src/config-lint/agents-lint.js +87 -0
- package/v3/@swarmdo/cli/dist/src/config-lint/lint.d.ts +3 -0
- package/v3/@swarmdo/cli/dist/src/config-lint/lint.js +3 -0
- package/v3/@swarmdo/cli/dist/src/coupling/coupling.d.ts +56 -0
- package/v3/@swarmdo/cli/dist/src/coupling/coupling.js +86 -0
- package/v3/@swarmdo/cli/dist/src/hooks-recipe/command-guard.d.ts +46 -0
- package/v3/@swarmdo/cli/dist/src/hooks-recipe/command-guard.js +120 -0
- package/v3/@swarmdo/cli/dist/src/hooks-recipe/recipe.js +8 -0
- package/v3/@swarmdo/cli/dist/src/mcp-client.js +2 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/coupling-tools.d.ts +14 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/coupling-tools.js +56 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.d.ts +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.js +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/profiles.js +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/task-deps.d.ts +35 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/task-deps.js +87 -0
- package/v3/@swarmdo/cli/dist/src/redact/redact.js +22 -10
- package/v3/@swarmdo/cli/dist/src/redact/sarif.d.ts +28 -0
- package/v3/@swarmdo/cli/dist/src/redact/sarif.js +53 -0
- package/v3/@swarmdo/cli/dist/src/release/release.d.ts +19 -0
- package/v3/@swarmdo/cli/dist/src/release/release.js +28 -0
- package/v3/@swarmdo/cli/dist/src/usage/reflect.d.ts +9 -1
- package/v3/@swarmdo/cli/dist/src/usage/reflect.js +14 -4
- package/v3/@swarmdo/cli/dist/src/util/since.d.ts +13 -0
- package/v3/@swarmdo/cli/dist/src/util/since.js +21 -0
- package/v3/@swarmdo/cli/package.json +1 -1
|
@@ -22,6 +22,7 @@ export { envTools } from './env-tools.js';
|
|
|
22
22
|
export { licenseTools } from './license-tools.js';
|
|
23
23
|
export { applyTools } from './apply-tools.js';
|
|
24
24
|
export { hotspotsTools } from './hotspots-tools.js';
|
|
25
|
+
export { couplingTools } from './coupling-tools.js';
|
|
25
26
|
export { affectedTools } from './affected-tools.js';
|
|
26
27
|
export { cyclesTools } from './cycles-tools.js';
|
|
27
28
|
export { testreportTools } from './testreport-tools.js';
|
|
@@ -21,6 +21,7 @@ export { envTools } from './env-tools.js';
|
|
|
21
21
|
export { licenseTools } from './license-tools.js';
|
|
22
22
|
export { applyTools } from './apply-tools.js';
|
|
23
23
|
export { hotspotsTools } from './hotspots-tools.js';
|
|
24
|
+
export { couplingTools } from './coupling-tools.js';
|
|
24
25
|
export { affectedTools } from './affected-tools.js';
|
|
25
26
|
export { cyclesTools } from './cycles-tools.js';
|
|
26
27
|
export { testreportTools } from './testreport-tools.js';
|
|
@@ -27,6 +27,7 @@ const LEAN_GROUPS = [
|
|
|
27
27
|
'env', // env_check — env-var drift (missing/unused/undocumented)
|
|
28
28
|
'apply', // apply_patch — fuzzy unified-diff applier for agent edits
|
|
29
29
|
'hotspots', // hotspots — git-history change-risk ranking ("where's the debt?")
|
|
30
|
+
'coupling', // coupling — files that change together (temporal co-change), complements affected
|
|
30
31
|
'affected', // affected — impacted files/tests from a change via the import graph
|
|
31
32
|
'cycles', // cycles — circular-import detection via the import graph
|
|
32
33
|
'testreport', // testreport — JUnit/TAP → structured failure digest (pairs w/ repair)
|
|
@@ -42,6 +42,41 @@ export declare function readyTasks(store: TaskStore): TaskRecord[];
|
|
|
42
42
|
export declare function blockedTasks(store: TaskStore): BlockedInfo[];
|
|
43
43
|
/** Tasks that became ready exactly because `completedTaskId` completed. */
|
|
44
44
|
export declare function unblockedBy(store: TaskStore, completedTaskId: string): TaskRecord[];
|
|
45
|
+
export interface DeadBlock {
|
|
46
|
+
task: TaskRecord;
|
|
47
|
+
/**
|
|
48
|
+
* The terminal-dead dependency ids in this task's dependency closure — the
|
|
49
|
+
* failed, cancelled, or missing tasks that make it permanently unreachable
|
|
50
|
+
* until a human retries/cancels/creates them. Deterministic (sorted).
|
|
51
|
+
*/
|
|
52
|
+
rootCause: string[];
|
|
53
|
+
}
|
|
54
|
+
export interface TaskDagHealth {
|
|
55
|
+
/** pending tasks whose unmet deps can STILL complete — a transient wait */
|
|
56
|
+
liveBlocked: TaskRecord[];
|
|
57
|
+
/** pending tasks that can NEVER become ready without human intervention */
|
|
58
|
+
deadBlocked: DeadBlock[];
|
|
59
|
+
/**
|
|
60
|
+
* true when pending work exists but nothing is ready AND nothing is
|
|
61
|
+
* in_progress — the DAG cannot advance on its own. A coordinator looping on
|
|
62
|
+
* `task dispatch` would otherwise spin forever doing nothing.
|
|
63
|
+
*/
|
|
64
|
+
deadlocked: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Classify every blocked task as live- vs dead-blocked and report a global
|
|
68
|
+
* deadlock verdict. Pure.
|
|
69
|
+
*
|
|
70
|
+
* This is Apache Airflow's `upstream_failed` state + DAG-deadlock detection,
|
|
71
|
+
* ported onto the beads-style task DAG (see the module header): a dependency
|
|
72
|
+
* that failed / was cancelled / is missing NEVER satisfies (only 'completed'
|
|
73
|
+
* does), so every task transitively downstream is permanently stuck until a
|
|
74
|
+
* human intervenes. `blockedTasks()` reports the wait but can't tell a
|
|
75
|
+
* transient wait from a permanent one — the dispatcher then skips a
|
|
76
|
+
* failed-dep block every pass, indistinguishable from a normal wait, and the
|
|
77
|
+
* DAG quietly wedges. This turns that silent stall into a named diagnostic.
|
|
78
|
+
*/
|
|
79
|
+
export declare function taskDagHealth(store: TaskStore): TaskDagHealth;
|
|
45
80
|
/**
|
|
46
81
|
* Flat dependency view: one line per task, dependencies inline with their
|
|
47
82
|
* states. A DAG has no single tree; a flat annotated list stays honest for
|
|
@@ -94,6 +94,93 @@ export function blockedTasks(store) {
|
|
|
94
94
|
export function unblockedBy(store, completedTaskId) {
|
|
95
95
|
return Object.values(store.tasks).filter((t) => t.status === 'pending' && (t.dependsOn ?? []).includes(completedTaskId) && isReady(store, t));
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Classify every blocked task as live- vs dead-blocked and report a global
|
|
99
|
+
* deadlock verdict. Pure.
|
|
100
|
+
*
|
|
101
|
+
* This is Apache Airflow's `upstream_failed` state + DAG-deadlock detection,
|
|
102
|
+
* ported onto the beads-style task DAG (see the module header): a dependency
|
|
103
|
+
* that failed / was cancelled / is missing NEVER satisfies (only 'completed'
|
|
104
|
+
* does), so every task transitively downstream is permanently stuck until a
|
|
105
|
+
* human intervenes. `blockedTasks()` reports the wait but can't tell a
|
|
106
|
+
* transient wait from a permanent one — the dispatcher then skips a
|
|
107
|
+
* failed-dep block every pass, indistinguishable from a normal wait, and the
|
|
108
|
+
* DAG quietly wedges. This turns that silent stall into a named diagnostic.
|
|
109
|
+
*/
|
|
110
|
+
export function taskDagHealth(store) {
|
|
111
|
+
// canComplete(id): can this task ever reach status 'completed'?
|
|
112
|
+
// completed → yes; failed/cancelled/missing → no; pending/in_progress →
|
|
113
|
+
// only if EVERY dependency can complete. Memoized; a cycle (the store is
|
|
114
|
+
// acyclic by invariant, but guard anyway) resolves to false.
|
|
115
|
+
const memo = new Map();
|
|
116
|
+
const visiting = new Set();
|
|
117
|
+
const canComplete = (id) => {
|
|
118
|
+
const cached = memo.get(id);
|
|
119
|
+
if (cached !== undefined)
|
|
120
|
+
return cached;
|
|
121
|
+
const task = store.tasks[id];
|
|
122
|
+
if (!task)
|
|
123
|
+
return false; // missing id — blocks forever
|
|
124
|
+
if (task.status === 'completed')
|
|
125
|
+
return true;
|
|
126
|
+
if (task.status === 'failed' || task.status === 'cancelled')
|
|
127
|
+
return false;
|
|
128
|
+
if (visiting.has(id))
|
|
129
|
+
return false; // cycle guard
|
|
130
|
+
visiting.add(id);
|
|
131
|
+
const ok = (task.dependsOn ?? []).every((d) => canComplete(d));
|
|
132
|
+
visiting.delete(id);
|
|
133
|
+
memo.set(id, ok);
|
|
134
|
+
return ok;
|
|
135
|
+
};
|
|
136
|
+
// The terminal-dead ids (failed/cancelled/missing) reachable from a task,
|
|
137
|
+
// descending THROUGH still-pending intermediates to name the real culprit.
|
|
138
|
+
const deadRoots = (task) => {
|
|
139
|
+
const found = new Set();
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
const stack = [...(task.dependsOn ?? [])];
|
|
142
|
+
while (stack.length) {
|
|
143
|
+
const id = stack.pop();
|
|
144
|
+
if (seen.has(id))
|
|
145
|
+
continue;
|
|
146
|
+
seen.add(id);
|
|
147
|
+
const dep = store.tasks[id];
|
|
148
|
+
if (!dep) {
|
|
149
|
+
found.add(id);
|
|
150
|
+
continue;
|
|
151
|
+
} // missing
|
|
152
|
+
if (dep.status === 'failed' || dep.status === 'cancelled') {
|
|
153
|
+
found.add(id);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (dep.status === 'completed')
|
|
157
|
+
continue; // satisfied — not a culprit
|
|
158
|
+
for (const d of dep.dependsOn ?? [])
|
|
159
|
+
stack.push(d); // pending/in_progress → descend
|
|
160
|
+
}
|
|
161
|
+
return [...found].sort();
|
|
162
|
+
};
|
|
163
|
+
const liveBlocked = [];
|
|
164
|
+
const deadBlocked = [];
|
|
165
|
+
for (const task of Object.values(store.tasks)) {
|
|
166
|
+
if (task.status !== 'pending')
|
|
167
|
+
continue;
|
|
168
|
+
const deps = task.dependsOn ?? [];
|
|
169
|
+
if (deps.length === 0)
|
|
170
|
+
continue; // ready (dep-free), not blocked
|
|
171
|
+
if (deps.every((id) => satisfies(store.tasks[id])))
|
|
172
|
+
continue; // ready, not blocked
|
|
173
|
+
if (deps.every((id) => canComplete(id)))
|
|
174
|
+
liveBlocked.push(task);
|
|
175
|
+
else
|
|
176
|
+
deadBlocked.push({ task, rootCause: deadRoots(task) });
|
|
177
|
+
}
|
|
178
|
+
const all = Object.values(store.tasks);
|
|
179
|
+
const anyPending = all.some((t) => t.status === 'pending');
|
|
180
|
+
const anyInProgress = all.some((t) => t.status === 'in_progress');
|
|
181
|
+
const deadlocked = anyPending && readyTasks(store).length === 0 && !anyInProgress;
|
|
182
|
+
return { liveBlocked, deadBlocked, deadlocked };
|
|
183
|
+
}
|
|
97
184
|
const STATUS_MARK = {
|
|
98
185
|
completed: '✔',
|
|
99
186
|
in_progress: '▶',
|
|
@@ -44,25 +44,37 @@ export const RULES = [
|
|
|
44
44
|
* Keyword set mirrors gitleaks' reference `generic-api-key` rule (adds bare
|
|
45
45
|
* `key`, `credential`, `creds` so `PRIVATE_KEY=`/`ENCRYPTION_KEY=`/`DB_CREDENTIAL=`
|
|
46
46
|
* secrets reach the entropy check). The `[:=]` immediately after the keyword
|
|
47
|
-
* anchors it, so `keyboard=`/`monkey_val=` don't match
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
47
|
+
* anchors it, so `keyboard=`/`monkey_val=` don't match. A `(?<![A-Za-z])` guard
|
|
48
|
+
* requires the keyword NOT be glued to a preceding letter, so a common word that
|
|
49
|
+
* merely ENDS in a keyword — `monkey=`/`donkey=`/`whiskey=`/`turkey=` (end in
|
|
50
|
+
* `key`), `compass=`/`bypass=` (end in `pass`) — is no longer flagged as a
|
|
51
|
+
* secret, while a `_`/`-`-separated name (`PRIVATE_KEY`, `x-api-key`) still
|
|
52
|
+
* matches (`_`/`-` aren't letters). The entropy + length gates keep low-entropy
|
|
53
|
+
* values (paths, short flags) out. The value class also stops at the URL/query
|
|
54
|
+
* delimiters `&`, `?`, `#` (absent from base64/base64url/hex token alphabets):
|
|
55
|
+
* otherwise a query-string secret like `client_secret=…&api_key=…` over-captures
|
|
56
|
+
* past the `&` into the next secret, and that inflated span — overlapping a range
|
|
57
|
+
* the high-confidence RULES pass already claimed — gets dropped entirely, leaving
|
|
58
|
+
* the first secret unredacted.
|
|
54
59
|
*/
|
|
55
|
-
const ASSIGNMENT_RE = /(?:pass(?:word|phrase|wd)?|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|auth[_-]?token|client[_-]?secret|credential|creds|key)["']?\s*[:=]\s*["']?([^\s"'`,;&?#]{8,})/gi;
|
|
60
|
+
const ASSIGNMENT_RE = /(?<![A-Za-z])(?:pass(?:word|phrase|wd)?|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|auth[_-]?token|client[_-]?secret|credential|creds|key)["']?\s*[:=]\s*["']?([^\s"'`,;&?#]{8,})/gi;
|
|
56
61
|
/** Shannon entropy in bits/char. Pure; used by the assignment fallback. */
|
|
57
62
|
export function shannonEntropy(s) {
|
|
58
63
|
if (!s)
|
|
59
64
|
return 0;
|
|
65
|
+
// Iterate AND normalize by code points, not UTF-16 code units. `for..of` /
|
|
66
|
+
// spread yield one symbol per code point, but `s.length` counts an astral
|
|
67
|
+
// char (emoji, CJK Ext-B, math alphanumerics) as 2 units — so dividing the
|
|
68
|
+
// per-symbol counts by s.length made the probabilities sum to <1 and
|
|
69
|
+
// under-counted entropy up to ~2×, letting a high-entropy secret with astral
|
|
70
|
+
// chars fall under the threshold and pass through UN-redacted (#95).
|
|
71
|
+
const cps = [...s];
|
|
60
72
|
const freq = new Map();
|
|
61
|
-
for (const ch of
|
|
73
|
+
for (const ch of cps)
|
|
62
74
|
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
63
75
|
let bits = 0;
|
|
64
76
|
for (const n of freq.values()) {
|
|
65
|
-
const p = n /
|
|
77
|
+
const p = n / cps.length;
|
|
66
78
|
bits -= p * Math.log2(p);
|
|
67
79
|
}
|
|
68
80
|
return bits;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sarif.ts — render redact secret-scan findings as a SARIF 2.1.0 document.
|
|
3
|
+
*
|
|
4
|
+
* SARIF (Static Analysis Results Interchange Format) is a ratified OASIS
|
|
5
|
+
* standard that GitHub code-scanning ingests via `github/codeql-action/
|
|
6
|
+
* upload-sarif`. Emit it from `redact --scan` in CI and leaked-secret findings
|
|
7
|
+
* surface as alerts in the Security tab + PR annotations instead of dying in
|
|
8
|
+
* the build log — the same thing gitleaks/trufflehog/ggshield do with their
|
|
9
|
+
* `--report-format sarif`.
|
|
10
|
+
*
|
|
11
|
+
* Pure + deterministic: findings in, JSON string out. No fs/network.
|
|
12
|
+
*/
|
|
13
|
+
import type { Finding } from './redact.js';
|
|
14
|
+
export interface SarifOptions {
|
|
15
|
+
/** SARIF tool.driver.name (default 'swarmdo-redact') */
|
|
16
|
+
toolName?: string;
|
|
17
|
+
/** SARIF tool.driver.version — omitted from the document when not given */
|
|
18
|
+
toolVersion?: string;
|
|
19
|
+
/**
|
|
20
|
+
* URI for the artifact each finding lives in. `redact --scan` reads a STREAM
|
|
21
|
+
* (stdin / wrapped output), so there is no committed file by default; pass
|
|
22
|
+
* this (e.g. from `--source <path>`) to anchor the alerts onto a repo path.
|
|
23
|
+
*/
|
|
24
|
+
artifactUri?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Render `findings` as a SARIF 2.1.0 document string. Pure + deterministic. */
|
|
27
|
+
export declare function toSarif(findings: Finding[], opts?: SarifOptions): string;
|
|
28
|
+
//# sourceMappingURL=sarif.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sarif.ts — render redact secret-scan findings as a SARIF 2.1.0 document.
|
|
3
|
+
*
|
|
4
|
+
* SARIF (Static Analysis Results Interchange Format) is a ratified OASIS
|
|
5
|
+
* standard that GitHub code-scanning ingests via `github/codeql-action/
|
|
6
|
+
* upload-sarif`. Emit it from `redact --scan` in CI and leaked-secret findings
|
|
7
|
+
* surface as alerts in the Security tab + PR annotations instead of dying in
|
|
8
|
+
* the build log — the same thing gitleaks/trufflehog/ggshield do with their
|
|
9
|
+
* `--report-format sarif`.
|
|
10
|
+
*
|
|
11
|
+
* Pure + deterministic: findings in, JSON string out. No fs/network.
|
|
12
|
+
*/
|
|
13
|
+
const SARIF_SCHEMA = 'https://json.schemastore.org/sarif-2.1.0.json';
|
|
14
|
+
const INFO_URI = 'the upstream project (see NOTICE)';
|
|
15
|
+
/** Render `findings` as a SARIF 2.1.0 document string. Pure + deterministic. */
|
|
16
|
+
export function toSarif(findings, opts = {}) {
|
|
17
|
+
const toolName = opts.toolName ?? 'swarmdo-redact';
|
|
18
|
+
// One rule descriptor per distinct ruleId, in first-seen order. Findings
|
|
19
|
+
// carry their own description, so the entropy fallback (not in RULES) is
|
|
20
|
+
// covered too — every result.ruleId is guaranteed to appear in rules[].
|
|
21
|
+
const ruleIndex = new Map();
|
|
22
|
+
const rules = [];
|
|
23
|
+
for (const f of findings) {
|
|
24
|
+
if (ruleIndex.has(f.ruleId))
|
|
25
|
+
continue;
|
|
26
|
+
ruleIndex.set(f.ruleId, rules.length);
|
|
27
|
+
rules.push({ id: f.ruleId, name: f.ruleId, shortDescription: { text: f.description } });
|
|
28
|
+
}
|
|
29
|
+
const results = findings.map((f) => {
|
|
30
|
+
const physicalLocation = {
|
|
31
|
+
region: { startLine: f.line, startColumn: f.column },
|
|
32
|
+
};
|
|
33
|
+
if (opts.artifactUri)
|
|
34
|
+
physicalLocation.artifactLocation = { uri: opts.artifactUri };
|
|
35
|
+
return {
|
|
36
|
+
ruleId: f.ruleId,
|
|
37
|
+
ruleIndex: ruleIndex.get(f.ruleId),
|
|
38
|
+
level: 'error',
|
|
39
|
+
message: { text: `Possible secret: ${f.description}` },
|
|
40
|
+
locations: [{ physicalLocation }],
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
const driver = { name: toolName, informationUri: INFO_URI, rules };
|
|
44
|
+
if (opts.toolVersion)
|
|
45
|
+
driver.version = opts.toolVersion;
|
|
46
|
+
const doc = {
|
|
47
|
+
$schema: SARIF_SCHEMA,
|
|
48
|
+
version: '2.1.0',
|
|
49
|
+
runs: [{ tool: { driver }, results }],
|
|
50
|
+
};
|
|
51
|
+
return JSON.stringify(doc, null, 2);
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=sarif.js.map
|
|
@@ -69,4 +69,23 @@ export declare const DOC_FILES: string[];
|
|
|
69
69
|
export declare function planRelease(input: ReleaseInput): ReleasePlan;
|
|
70
70
|
/** One-line human rendering of a step (for the dry-run plan). */
|
|
71
71
|
export declare function renderStep(s: ReleaseStep): string;
|
|
72
|
+
export interface SiteIdentity {
|
|
73
|
+
name: string;
|
|
74
|
+
email: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the committer identity for the throwaway swarmdo.com clone. That
|
|
78
|
+
* clone inherits no identity, and a machine may have only a *local* (per-repo)
|
|
79
|
+
* git user — so `git commit` there dies with "Author identity unknown" unless
|
|
80
|
+
* we inject one. Prefer the operator's configured identity; fall back to the
|
|
81
|
+
* Swarmdo bot so the site deploy never fails on a missing global git config.
|
|
82
|
+
* `readConfig` is injected (real: `git -C <root> config <key>`) so this stays
|
|
83
|
+
* pure and unit-testable.
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveSiteIdentity(readConfig: (key: string) => string | undefined): SiteIdentity;
|
|
86
|
+
/**
|
|
87
|
+
* `git commit` argv for the site sync — injects the identity via `-c` so the
|
|
88
|
+
* commit succeeds without a global git user, and keeps the bot co-author trailer.
|
|
89
|
+
*/
|
|
90
|
+
export declare function siteCommitArgs(version: string, id: SiteIdentity): string[];
|
|
72
91
|
//# sourceMappingURL=release.d.ts.map
|
|
@@ -83,4 +83,32 @@ export function renderStep(s) {
|
|
|
83
83
|
case 'deploy-site': return `deploy website → SwarmDo/swarmdo.com (copy working copy, push, curl-verify swarmdo.com serves ${s.version})`;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the committer identity for the throwaway swarmdo.com clone. That
|
|
88
|
+
* clone inherits no identity, and a machine may have only a *local* (per-repo)
|
|
89
|
+
* git user — so `git commit` there dies with "Author identity unknown" unless
|
|
90
|
+
* we inject one. Prefer the operator's configured identity; fall back to the
|
|
91
|
+
* Swarmdo bot so the site deploy never fails on a missing global git config.
|
|
92
|
+
* `readConfig` is injected (real: `git -C <root> config <key>`) so this stays
|
|
93
|
+
* pure and unit-testable.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveSiteIdentity(readConfig) {
|
|
96
|
+
const name = (readConfig('user.name') || '').trim();
|
|
97
|
+
const email = (readConfig('user.email') || '').trim();
|
|
98
|
+
return {
|
|
99
|
+
name: name || 'Swarmdo',
|
|
100
|
+
email: email || 'maintainers@swarmdo.com',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* `git commit` argv for the site sync — injects the identity via `-c` so the
|
|
105
|
+
* commit succeeds without a global git user, and keeps the bot co-author trailer.
|
|
106
|
+
*/
|
|
107
|
+
export function siteCommitArgs(version, id) {
|
|
108
|
+
return [
|
|
109
|
+
'-c', `user.name=${id.name}`,
|
|
110
|
+
'-c', `user.email=${id.email}`,
|
|
111
|
+
'commit', '-m', `release: sync site for v${version}\n\nCo-Authored-By: Swarmdo <maintainers@swarmdo.com>`,
|
|
112
|
+
];
|
|
113
|
+
}
|
|
86
114
|
//# sourceMappingURL=release.js.map
|
|
@@ -90,7 +90,15 @@ export declare function nextDay(iso: string): string;
|
|
|
90
90
|
* Pure. Used to derive a `--period 1m|3m|…` window start from today.
|
|
91
91
|
*/
|
|
92
92
|
export declare function monthsBefore(iso: string, n: number): string;
|
|
93
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Inclusive whole-day span between two ISO dates (from <= to). Pure.
|
|
95
|
+
*
|
|
96
|
+
* Counts on the UTC timeline (`Date.UTC`), NOT local midnights: a local
|
|
97
|
+
* `new Date('…T00:00:00')` is DST-sensitive, so a range crossing a spring-forward
|
|
98
|
+
* day (only 23h long) would floor-divide to one day short — e.g. under
|
|
99
|
+
* TZ=America/New_York a full March would read 30. UTC calendar midnights are
|
|
100
|
+
* always an exact 86.4M ms apart, so this is correct in every process timezone.
|
|
101
|
+
*/
|
|
94
102
|
export declare function spanDays(from: string, to: string): number;
|
|
95
103
|
/** Longest run of consecutive calendar days present in the set. Pure. */
|
|
96
104
|
export declare function longestStreakOf(days: string[]): number;
|
|
@@ -40,11 +40,21 @@ export function monthsBefore(iso, n) {
|
|
|
40
40
|
const dd = String(dt.getDate()).padStart(2, '0');
|
|
41
41
|
return `${yy}-${mm}-${dd}`;
|
|
42
42
|
}
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Inclusive whole-day span between two ISO dates (from <= to). Pure.
|
|
45
|
+
*
|
|
46
|
+
* Counts on the UTC timeline (`Date.UTC`), NOT local midnights: a local
|
|
47
|
+
* `new Date('…T00:00:00')` is DST-sensitive, so a range crossing a spring-forward
|
|
48
|
+
* day (only 23h long) would floor-divide to one day short — e.g. under
|
|
49
|
+
* TZ=America/New_York a full March would read 30. UTC calendar midnights are
|
|
50
|
+
* always an exact 86.4M ms apart, so this is correct in every process timezone.
|
|
51
|
+
*/
|
|
44
52
|
export function spanDays(from, to) {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
53
|
+
const [ay, am, ad] = from.split('-').map(Number);
|
|
54
|
+
const [by, bm, bd] = to.split('-').map(Number);
|
|
55
|
+
const a = Date.UTC(ay, am - 1, ad);
|
|
56
|
+
const b = Date.UTC(by, bm - 1, bd);
|
|
57
|
+
return Math.round((b - a) / 86_400_000) + 1;
|
|
48
58
|
}
|
|
49
59
|
/** Longest run of consecutive calendar days present in the set. Pure. */
|
|
50
60
|
export function longestStreakOf(days) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* since.ts — normalize compact age windows for `git log --since`.
|
|
3
|
+
*
|
|
4
|
+
* git's approxidate parser does NOT understand the bare compact form `90d`
|
|
5
|
+
* (it silently matches nothing — `git log --since=90d` returns zero commits),
|
|
6
|
+
* yet `hotspots`/`coupling` document `--since 90d`. This maps the compact forms
|
|
7
|
+
* (`90d`, `6mo`, `2w`, `1y`, `3h`) to the spelled-out `"90 days ago"` git DOES
|
|
8
|
+
* understand. Anything already spelled out (`"3 months ago"`), an ISO date, or
|
|
9
|
+
* an unrecognized token passes through unchanged. Pure + unit-tested.
|
|
10
|
+
*/
|
|
11
|
+
/** Convert `90d`→`90 days ago`, `6mo`→`6 months ago`, etc. Passthrough otherwise. */
|
|
12
|
+
export declare function normalizeSince(since: string): string;
|
|
13
|
+
//# sourceMappingURL=since.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* since.ts — normalize compact age windows for `git log --since`.
|
|
3
|
+
*
|
|
4
|
+
* git's approxidate parser does NOT understand the bare compact form `90d`
|
|
5
|
+
* (it silently matches nothing — `git log --since=90d` returns zero commits),
|
|
6
|
+
* yet `hotspots`/`coupling` document `--since 90d`. This maps the compact forms
|
|
7
|
+
* (`90d`, `6mo`, `2w`, `1y`, `3h`) to the spelled-out `"90 days ago"` git DOES
|
|
8
|
+
* understand. Anything already spelled out (`"3 months ago"`), an ISO date, or
|
|
9
|
+
* an unrecognized token passes through unchanged. Pure + unit-tested.
|
|
10
|
+
*/
|
|
11
|
+
const COMPACT = /^\s*(\d+)\s*(d|days?|w|wks?|weeks?|mo|mons?|months?|y|yrs?|years?|h|hrs?|hours?)\s*$/i;
|
|
12
|
+
/** Convert `90d`→`90 days ago`, `6mo`→`6 months ago`, etc. Passthrough otherwise. */
|
|
13
|
+
export function normalizeSince(since) {
|
|
14
|
+
const m = COMPACT.exec(since);
|
|
15
|
+
if (!m)
|
|
16
|
+
return since;
|
|
17
|
+
const c0 = m[2][0].toLowerCase(); // the allowed units are uniquely keyed by first letter
|
|
18
|
+
const unit = c0 === 'd' ? 'days' : c0 === 'w' ? 'weeks' : c0 === 'y' ? 'years' : c0 === 'h' ? 'hours' : 'months';
|
|
19
|
+
return `${m[1]} ${unit} ago`;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=since.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmdo/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Swarmdo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|