@clipboard-health/groundcrew 4.21.0 → 4.23.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 +1 -1
- package/crew.config.example.ts +10 -9
- package/dist/commands/init.js +5 -3
- package/dist/commands/orchestrator.d.ts.map +1 -1
- package/dist/commands/orchestrator.js +20 -7
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +2 -4
- package/dist/lib/buildSources.d.ts +7 -18
- package/dist/lib/buildSources.d.ts.map +1 -1
- package/dist/lib/buildSources.js +9 -48
- package/package.json +1 -1
- package/static/groundcrew-avatar.png +0 -0
- package/static/groundcrew-emoji.gif +0 -0
- package/static/groundcrew-mark.svg +29 -0
- package/static/groundcrew-wordmark-dark.svg +28 -5
- package/static/groundcrew-wordmark-light.svg +28 -5
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
<a href="https://www.npmjs.com/package/@clipboard-health/groundcrew"><img alt="npm" src="https://img.shields.io/npm/v/@clipboard-health/groundcrew?style=flat-square&label=npm&color=
|
|
13
|
+
<a href="https://www.npmjs.com/package/@clipboard-health/groundcrew"><img alt="npm" src="https://img.shields.io/npm/v/@clipboard-health/groundcrew?style=flat-square&label=npm&color=FF6D00&labelColor=18181b"></a>
|
|
14
14
|
<a href="https://www.npmjs.com/package/@clipboard-health/groundcrew"><img alt="downloads" src="https://img.shields.io/npm/dw/@clipboard-health/groundcrew?style=flat-square&label=downloads&color=18181b&labelColor=18181b"></a>
|
|
15
15
|
<a href="https://github.com/ClipboardHealth/groundcrew/actions/workflows/ci.yml"><img alt="ci" src="https://img.shields.io/github/actions/workflow/status/ClipboardHealth/groundcrew/ci.yml?style=flat-square&label=ci&color=77d94e&labelColor=18181b"></a>
|
|
16
16
|
<a href="./LICENSE"><img alt="license" src="https://img.shields.io/npm/l/@clipboard-health/groundcrew?style=flat-square&label=license&color=18181b&labelColor=18181b"></a>
|
package/crew.config.example.ts
CHANGED
|
@@ -2,16 +2,17 @@ import type { Config } from "@clipboard-health/groundcrew";
|
|
|
2
2
|
// import { readFileSync } from "node:fs";
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// carries an `agent-*` label. There is no project / view block. The default
|
|
8
|
-
// Linear status names `In Progress` and `In Review` disambiguate Linear's
|
|
9
|
-
// `started` workflow states; other statuses fall back to workflow
|
|
10
|
-
// `state.type` (`unstarted` → todo, `started` → in progress,
|
|
11
|
-
// `completed`/`canceled`/`duplicate` → terminal).
|
|
5
|
+
// Task sources — at least one is required; add a `sources` array to get
|
|
6
|
+
// started. Two built-in options:
|
|
12
7
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
8
|
+
// Zero credentials: use a local todo.txt file. No API key needed.
|
|
9
|
+
// sources: [{ kind: "todo-txt" }]
|
|
10
|
+
//
|
|
11
|
+
// Linear: picks up issues assigned to your API key's viewer that carry
|
|
12
|
+
// an `agent-*` label. Requires GROUNDCREW_LINEAR_API_KEY.
|
|
13
|
+
// sources: [{ kind: "linear" }]
|
|
14
|
+
//
|
|
15
|
+
// Running `crew run` without a sources array will print this guidance and exit.
|
|
15
16
|
workspace: {
|
|
16
17
|
// Parent directory under which groundcrew clones repositories and (by
|
|
17
18
|
// default) creates per-task worktrees.
|
package/dist/commands/init.js
CHANGED
|
@@ -207,9 +207,11 @@ function writeInitGuidance(destination, options) {
|
|
|
207
207
|
writeOutput(" - Set workspace.projectDir and workspace.knownRepositories");
|
|
208
208
|
}
|
|
209
209
|
writeCloneGuidance(options);
|
|
210
|
-
writeOutput(" -
|
|
211
|
-
writeOutput(
|
|
212
|
-
writeOutput(
|
|
210
|
+
writeOutput(" - Add a task source to your config (required):");
|
|
211
|
+
writeOutput(" # Zero credentials — uses a local todo.txt file:");
|
|
212
|
+
writeOutput(' sources: [{ kind: "todo-txt" }]');
|
|
213
|
+
writeOutput(" # Or use Linear (requires GROUNDCREW_LINEAR_API_KEY):");
|
|
214
|
+
writeOutput(' sources: [{ kind: "linear" }]');
|
|
213
215
|
writeOutput(" - Validate and start:");
|
|
214
216
|
writeOutput(" crew doctor");
|
|
215
217
|
writeOutput(" crew run --watch");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/commands/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA2DH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACjB;AAiBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/commands/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA2DH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACjB;AAiBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgE7E"}
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createBoard } from "../lib/board.js";
|
|
8
8
|
import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
|
|
9
|
-
import {
|
|
9
|
+
import { loadConfigWithSource } from "../lib/config.js";
|
|
10
10
|
import { findPullRequestsForBranch } from "../lib/pullRequests.js";
|
|
11
11
|
import { RepositoryResolutionError } from "../lib/taskSource.js";
|
|
12
12
|
import { getUsageByModel } from "../lib/usage.js";
|
|
13
|
-
import { errorMessage, log, sleep } from "../lib/util.js";
|
|
13
|
+
import { errorMessage, log, sleep, writeOutput } from "../lib/util.js";
|
|
14
14
|
import { worktrees } from "../lib/worktrees.js";
|
|
15
15
|
import { createCleaner } from "./cleaner.js";
|
|
16
16
|
import { createDispatcher } from "./dispatcher.js";
|
|
@@ -66,11 +66,24 @@ async function fetchUsageOrEmpty(config, signal) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
export async function orchestrate(options) {
|
|
69
|
-
const config = await
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
const { config, source: configSource } = await loadConfigWithSource();
|
|
70
|
+
const rawSources = sourcesFromConfig(config);
|
|
71
|
+
if (rawSources.length === 0) {
|
|
72
|
+
writeOutput([
|
|
73
|
+
"No task sources configured. Add a sources array to your config:",
|
|
74
|
+
"",
|
|
75
|
+
` Path: ${configSource.filepath}`,
|
|
76
|
+
"",
|
|
77
|
+
" # Zero credentials — uses a local todo.txt file:",
|
|
78
|
+
' sources: [{ kind: "todo-txt" }]',
|
|
79
|
+
"",
|
|
80
|
+
" # Or use Linear (requires GROUNDCREW_LINEAR_API_KEY):",
|
|
81
|
+
' sources: [{ kind: "linear" }]',
|
|
82
|
+
].join("\n"));
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const allSources = await buildSources(rawSources, { globalConfig: config });
|
|
74
87
|
const board = createBoard(allSources);
|
|
75
88
|
await board.verify();
|
|
76
89
|
const cleaner = createCleaner({ config });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAcnE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAcnE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAkqBD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
|
package/dist/commands/status.js
CHANGED
|
@@ -434,10 +434,8 @@ async function buildBoardForStatus(config) {
|
|
|
434
434
|
}
|
|
435
435
|
/**
|
|
436
436
|
* Single board fetch used by both the slot count header and the
|
|
437
|
-
* Queue/Blocked sections.
|
|
438
|
-
*
|
|
439
|
-
* (e.g., missing API key) are captured and rendered later as
|
|
440
|
-
* `unavailable: ...` in the Queue section.
|
|
437
|
+
* Queue/Blocked sections. Failures (e.g., missing API key) are captured
|
|
438
|
+
* and rendered later as `unavailable: ...` in the Queue section.
|
|
441
439
|
*/
|
|
442
440
|
async function fetchBoardForStatus(config) {
|
|
443
441
|
try {
|
|
@@ -22,27 +22,16 @@ export declare function buildSources(rawConfigs: readonly unknown[], context: Ad
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function buildSourcesWith(registry: Record<string, AdapterDefinition>, rawConfigs: readonly unknown[], context: AdapterContext): TaskSource[];
|
|
24
24
|
/**
|
|
25
|
-
* Build the runtime source list from a ResolvedConfig:
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* when the user already declared a Linear source — by `kind: "linear"`, or by
|
|
30
|
-
* a surviving (non-disabled) source whose runtime name is "linear" — so they
|
|
31
|
-
* can override its `name` / construction without spawning a duplicate adapter.
|
|
32
|
-
*
|
|
33
|
-
* Users opt out of Linear entirely with the sentinel
|
|
34
|
-
* `{ kind: "linear", enabled: false }`: it still counts as an explicit Linear
|
|
35
|
-
* declaration (so the implicit source is suppressed) and is itself filtered
|
|
36
|
-
* out (so no Linear adapter is constructed and no API key is required). Any
|
|
37
|
-
* other source with `enabled: false` is likewise dropped from the result.
|
|
25
|
+
* Build the runtime source list from a ResolvedConfig: returns the enabled
|
|
26
|
+
* entries from `config.sources`. Any source with `enabled: false` is dropped.
|
|
27
|
+
* Returns an empty array when no sources are configured — callers are
|
|
28
|
+
* responsible for detecting that state and guiding the user.
|
|
38
29
|
*/
|
|
39
30
|
export declare function sourcesFromConfig(config: ResolvedConfig): readonly unknown[];
|
|
40
31
|
/**
|
|
41
|
-
* True when
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* off. Derived from `sourcesFromConfig` so it honors both the explicit opt-out
|
|
45
|
-
* sentinel and the implicit-source synthesis.
|
|
32
|
+
* True when an enabled source explicitly declares `kind: "linear"`. Callers
|
|
33
|
+
* use this to skip Linear API calls (and the missing-API-key error they raise)
|
|
34
|
+
* when Linear is not configured.
|
|
46
35
|
*/
|
|
47
36
|
export declare function isLinearEnabled(config: ResolvedConfig): boolean;
|
|
48
37
|
//# sourceMappingURL=buildSources.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildSources.d.ts","sourceRoot":"","sources":["../../src/lib/buildSources.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,eAAO,MAAM,SAAS;;iBAAiC,CAAC;AAExD;;GAEG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,UAAU,EAAE,CAAC,CAGvB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAC3C,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,UAAU,EAAE,CAcd;
|
|
1
|
+
{"version":3,"file":"buildSources.d.ts","sourceRoot":"","sources":["../../src/lib/buildSources.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,eAAO,MAAM,SAAS;;iBAAiC,CAAC;AAExD;;GAEG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,UAAU,EAAE,CAAC,CAGvB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAC3C,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,UAAU,EAAE,CAcd;AA0CD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,OAAO,EAAE,CAE5E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAE/D"}
|
package/dist/lib/buildSources.js
CHANGED
|
@@ -63,63 +63,24 @@ function isSourceDisabled(raw) {
|
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* True when `raw` declares `kind: "linear"`, regardless of `name` or `enabled`.
|
|
66
|
-
*
|
|
67
|
-
* the implicit Linear source even though the entry itself is filtered out.
|
|
66
|
+
* Used by `isLinearEnabled` to detect explicitly configured Linear sources.
|
|
68
67
|
*/
|
|
69
68
|
function isLinearKindSource(raw) {
|
|
70
69
|
return sourceFields(raw).kind === "linear";
|
|
71
70
|
}
|
|
72
71
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* would otherwise collide with the implicit Linear source.
|
|
78
|
-
*
|
|
79
|
-
* Used to suppress the synthesized implicit Linear source so a renamed Linear
|
|
80
|
-
* entry like `{ kind: "linear", name: "custom" }` doesn't spawn a duplicate
|
|
81
|
-
* adapter pointed at the same viewer. Returns false for malformed entries
|
|
82
|
-
* (no `kind`/`name`) — those get rejected by the per-adapter Zod schema
|
|
83
|
-
* downstream.
|
|
84
|
-
*/
|
|
85
|
-
function isExplicitLinearSource(raw) {
|
|
86
|
-
const { kind, name } = sourceFields(raw);
|
|
87
|
-
return kind === "linear" || (name ?? kind) === "linear";
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Build the runtime source list from a ResolvedConfig: synthesizes the
|
|
91
|
-
* implicit Linear source (Linear is always active under the post-#110
|
|
92
|
-
* model — viewer + agent-* label filtering happens at the GraphQL layer)
|
|
93
|
-
* and appends any user-declared `sources`. The implicit source is omitted
|
|
94
|
-
* when the user already declared a Linear source — by `kind: "linear"`, or by
|
|
95
|
-
* a surviving (non-disabled) source whose runtime name is "linear" — so they
|
|
96
|
-
* can override its `name` / construction without spawning a duplicate adapter.
|
|
97
|
-
*
|
|
98
|
-
* Users opt out of Linear entirely with the sentinel
|
|
99
|
-
* `{ kind: "linear", enabled: false }`: it still counts as an explicit Linear
|
|
100
|
-
* declaration (so the implicit source is suppressed) and is itself filtered
|
|
101
|
-
* out (so no Linear adapter is constructed and no API key is required). Any
|
|
102
|
-
* other source with `enabled: false` is likewise dropped from the result.
|
|
72
|
+
* Build the runtime source list from a ResolvedConfig: returns the enabled
|
|
73
|
+
* entries from `config.sources`. Any source with `enabled: false` is dropped.
|
|
74
|
+
* Returns an empty array when no sources are configured — callers are
|
|
75
|
+
* responsible for detecting that state and guiding the user.
|
|
103
76
|
*/
|
|
104
77
|
export function sourcesFromConfig(config) {
|
|
105
|
-
|
|
106
|
-
// A `kind: "linear"` entry suppresses the implicit source even when it is the
|
|
107
|
-
// disabled opt-out sentinel — it's removed from `kept` above, leaving Linear
|
|
108
|
-
// off entirely. A source that's Linear only by *name* (e.g. a shell source
|
|
109
|
-
// named "linear") suppresses the implicit source only while it survives the
|
|
110
|
-
// filter, so disabling such an entry doesn't silently drop Linear.
|
|
111
|
-
const hasExplicitLinear = config.sources.some(isLinearKindSource) || kept.some(isExplicitLinearSource);
|
|
112
|
-
if (hasExplicitLinear) {
|
|
113
|
-
return kept;
|
|
114
|
-
}
|
|
115
|
-
return [{ kind: "linear" }, ...kept];
|
|
78
|
+
return config.sources.filter((source) => !isSourceDisabled(source));
|
|
116
79
|
}
|
|
117
80
|
/**
|
|
118
|
-
* True when
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* off. Derived from `sourcesFromConfig` so it honors both the explicit opt-out
|
|
122
|
-
* sentinel and the implicit-source synthesis.
|
|
81
|
+
* True when an enabled source explicitly declares `kind: "linear"`. Callers
|
|
82
|
+
* use this to skip Linear API calls (and the missing-API-key error they raise)
|
|
83
|
+
* when Linear is not configured.
|
|
123
84
|
*/
|
|
124
85
|
export function isLinearEnabled(config) {
|
|
125
86
|
return sourcesFromConfig(config).some(isLinearKindSource);
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120" role="img" aria-label="groundcrew">
|
|
3
|
+
<title>groundcrew</title>
|
|
4
|
+
<!--
|
|
5
|
+
Crossed marshaling wands. Handles at bottom (18,108) and (102,108),
|
|
6
|
+
cross at (60,50), tips at (88,11) and (32,11).
|
|
7
|
+
SMIL animation: wands alternate dimming, 2.4s cycle.
|
|
8
|
+
Right wand offset by -1.2s so both open fully opaque.
|
|
9
|
+
-->
|
|
10
|
+
<line x1="18" y1="108" x2="88" y2="11" stroke="#FF6D00" stroke-width="20" stroke-linecap="round">
|
|
11
|
+
<animate attributeName="opacity"
|
|
12
|
+
values="1;0.38;1;1"
|
|
13
|
+
keyTimes="0;0.25;0.5;1"
|
|
14
|
+
calcMode="spline"
|
|
15
|
+
keySplines="0.42 0 0.58 1;0.42 0 0.58 1;0 0 1 1"
|
|
16
|
+
dur="2.4s"
|
|
17
|
+
repeatCount="indefinite"/>
|
|
18
|
+
</line>
|
|
19
|
+
<line x1="102" y1="108" x2="32" y2="11" stroke="#FF6D00" stroke-width="20" stroke-linecap="round">
|
|
20
|
+
<animate attributeName="opacity"
|
|
21
|
+
values="1;0.38;1;1"
|
|
22
|
+
keyTimes="0;0.25;0.5;1"
|
|
23
|
+
calcMode="spline"
|
|
24
|
+
keySplines="0.42 0 0.58 1;0.42 0 0.58 1;0 0 1 1"
|
|
25
|
+
dur="2.4s"
|
|
26
|
+
begin="-1.2s"
|
|
27
|
+
repeatCount="indefinite"/>
|
|
28
|
+
</line>
|
|
29
|
+
</svg>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 680 120" width="680" height="120" role="img" aria-label="groundcrew">
|
|
3
|
+
<title>groundcrew</title>
|
|
3
4
|
<style>
|
|
4
5
|
.wordmark {
|
|
5
6
|
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", "DejaVu Sans Mono", monospace;
|
|
@@ -8,8 +9,30 @@
|
|
|
8
9
|
letter-spacing: -0.04em;
|
|
9
10
|
}
|
|
10
11
|
</style>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
<!--
|
|
13
|
+
Logomark: crossed marshaling wands.
|
|
14
|
+
Handles at (18,108) and (102,108), cross at (60,50), tips at (88,11) and (32,11).
|
|
15
|
+
SMIL animation: wands alternate dimming, 2.4s cycle.
|
|
16
|
+
Right wand offset by -1.2s so both open fully opaque.
|
|
17
|
+
-->
|
|
18
|
+
<line x1="18" y1="108" x2="88" y2="11" stroke="#FF6D00" stroke-width="20" stroke-linecap="round">
|
|
19
|
+
<animate attributeName="opacity"
|
|
20
|
+
values="1;0.38;1;1"
|
|
21
|
+
keyTimes="0;0.25;0.5;1"
|
|
22
|
+
calcMode="spline"
|
|
23
|
+
keySplines="0.42 0 0.58 1;0.42 0 0.58 1;0 0 1 1"
|
|
24
|
+
dur="2.4s"
|
|
25
|
+
repeatCount="indefinite"/>
|
|
26
|
+
</line>
|
|
27
|
+
<line x1="102" y1="108" x2="32" y2="11" stroke="#FF6D00" stroke-width="20" stroke-linecap="round">
|
|
28
|
+
<animate attributeName="opacity"
|
|
29
|
+
values="1;0.38;1;1"
|
|
30
|
+
keyTimes="0;0.25;0.5;1"
|
|
31
|
+
calcMode="spline"
|
|
32
|
+
keySplines="0.42 0 0.58 1;0.42 0 0.58 1;0 0 1 1"
|
|
33
|
+
dur="2.4s"
|
|
34
|
+
begin="-1.2s"
|
|
35
|
+
repeatCount="indefinite"/>
|
|
36
|
+
</line>
|
|
37
|
+
<text class="wordmark" x="136" y="92" textLength="540" lengthAdjust="spacingAndGlyphs" fill="#e4e4e7">groundcrew</text>
|
|
15
38
|
</svg>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 680 120" width="680" height="120" role="img" aria-label="groundcrew">
|
|
3
|
+
<title>groundcrew</title>
|
|
3
4
|
<style>
|
|
4
5
|
.wordmark {
|
|
5
6
|
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", "DejaVu Sans Mono", monospace;
|
|
@@ -8,8 +9,30 @@
|
|
|
8
9
|
letter-spacing: -0.04em;
|
|
9
10
|
}
|
|
10
11
|
</style>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
<!--
|
|
13
|
+
Logomark: crossed marshaling wands.
|
|
14
|
+
Handles at (18,108) and (102,108), cross at (60,50), tips at (88,11) and (32,11).
|
|
15
|
+
SMIL animation: wands alternate dimming, 2.4s cycle.
|
|
16
|
+
Right wand offset by -1.2s so both open fully opaque.
|
|
17
|
+
-->
|
|
18
|
+
<line x1="18" y1="108" x2="88" y2="11" stroke="#FF6D00" stroke-width="20" stroke-linecap="round">
|
|
19
|
+
<animate attributeName="opacity"
|
|
20
|
+
values="1;0.38;1;1"
|
|
21
|
+
keyTimes="0;0.25;0.5;1"
|
|
22
|
+
calcMode="spline"
|
|
23
|
+
keySplines="0.42 0 0.58 1;0.42 0 0.58 1;0 0 1 1"
|
|
24
|
+
dur="2.4s"
|
|
25
|
+
repeatCount="indefinite"/>
|
|
26
|
+
</line>
|
|
27
|
+
<line x1="102" y1="108" x2="32" y2="11" stroke="#FF6D00" stroke-width="20" stroke-linecap="round">
|
|
28
|
+
<animate attributeName="opacity"
|
|
29
|
+
values="1;0.38;1;1"
|
|
30
|
+
keyTimes="0;0.25;0.5;1"
|
|
31
|
+
calcMode="spline"
|
|
32
|
+
keySplines="0.42 0 0.58 1;0.42 0 0.58 1;0 0 1 1"
|
|
33
|
+
dur="2.4s"
|
|
34
|
+
begin="-1.2s"
|
|
35
|
+
repeatCount="indefinite"/>
|
|
36
|
+
</line>
|
|
37
|
+
<text class="wordmark" x="136" y="92" textLength="540" lengthAdjust="spacingAndGlyphs" fill="#18181b">groundcrew</text>
|
|
15
38
|
</svg>
|