@harnessforce/cli 0.3.0 → 0.4.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 +6 -0
- package/dist/bin.js +206 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,10 @@ harnessforce init
|
|
|
31
31
|
| `harnessforce otel-headers` | Prints the `Authorization` header for Claude Code's `otelHeadersHelper`. Exits 3 when there is no key, 4 when the destination differs from the pinned origin, 5 while the CLI version is unsupported, and 1 on other failures. On Linux, after printing a key read from the kernel keyring, it moves that Workspace's items to an unlocked Secret Service (not when the hook starts it), so a reboot does not lose them |
|
|
32
32
|
| `harnessforce run --issue <identifier> -- <agent> [args]` | Launches an agent with its session linked to an Issue |
|
|
33
33
|
| `harnessforce import` | Sends the metadata of past Claude Code sessions in connected repositories |
|
|
34
|
+
| `harnessforce status [--json]` | Shows the connected Workspace, your role, the setup steps that remain with their URLs, and whether the `origin` repository of the current directory is connected to the GitHub App. Reads only; the remote URL is compared on this machine and never sent. Exits 0 whenever the status was read |
|
|
34
35
|
| `harnessforce tune [--all] [--no-send] [--show-report] [--json]` | Analyzes past sessions on this machine by public rules and sends the counts (analysis reports) |
|
|
36
|
+
| `harnessforce tune --report-only [--all]` | Analyzes and sends like `harnessforce tune`, but writes nothing to stdout. Meant for your OS scheduler |
|
|
37
|
+
| `harnessforce tune --print-schedule` | Prints one cron line that runs `harnessforce tune --report-only` every Monday at 9:00 local time. Changes nothing and makes no network request |
|
|
35
38
|
| `harnessforce tune record` / `harnessforce tune --purge` | Records a proposal from stdin / deletes everything under `~/.harnessforce/tune/` |
|
|
36
39
|
| `harnessforce top [--once] [--json] [--ascii] [--theme <auto\|light\|dark\|ansi>]` | Shows the Claude Code and Codex sessions on this machine in the terminal: state, tokens, context and tool failures. Reads only local files and never connects to Harnessforce |
|
|
37
40
|
| `harnessforce hook session-start` | The entry of Codex's `SessionStart` hook: reads `session_id`, `cwd` and `source` from stdin, registers the session, and prints the session ID for the agent's context. Always exits 0 |
|
|
@@ -295,6 +298,9 @@ role cannot use it.
|
|
|
295
298
|
- `--json` also lists, for each recorded proposal whose application was detected, the value of its
|
|
296
299
|
category in the 14 days before and after the detection time (`followups`). The difference is not
|
|
297
300
|
a cause. Once a `--json` run outputs a follow-up whose after period has ended, later runs omit it.
|
|
301
|
+
- To send weekly without opening Claude Code, add the line that `harnessforce tune --print-schedule`
|
|
302
|
+
prints to your crontab yourself. Harnessforce never registers it. To stop, remove the line or set
|
|
303
|
+
`send_report` to `false`.
|
|
298
304
|
- Everything it keeps is under `~/.harnessforce/tune/`; `harnessforce tune --purge` deletes it.
|
|
299
305
|
|
|
300
306
|
## License
|
package/dist/bin.js
CHANGED
|
@@ -1445,6 +1445,38 @@ Exit codes:
|
|
|
1445
1445
|
|
|
1446
1446
|
Examples:
|
|
1447
1447
|
harnessforce import
|
|
1448
|
+
`
|
|
1449
|
+
},
|
|
1450
|
+
status: {
|
|
1451
|
+
summary: "Show the Workspace setup steps that remain",
|
|
1452
|
+
text: `Usage: harnessforce status [--json]
|
|
1453
|
+
|
|
1454
|
+
When to use:
|
|
1455
|
+
After init, and any time you want to see which setup steps of the connected
|
|
1456
|
+
Workspace remain and whether this repository is connected to the GitHub App.
|
|
1457
|
+
|
|
1458
|
+
What it does:
|
|
1459
|
+
Reads the setup checklist of the connected Workspace and the list of
|
|
1460
|
+
connected repositories, and prints your role, each step (done, to do with its
|
|
1461
|
+
URL, or "?" when it cannot be read), and whether the repository of the current
|
|
1462
|
+
directory (its origin remote) is connected.
|
|
1463
|
+
|
|
1464
|
+
Options:
|
|
1465
|
+
--json Print the checklist and the repository as one JSON object.
|
|
1466
|
+
|
|
1467
|
+
Sends:
|
|
1468
|
+
Read requests with the API token. It writes nothing.
|
|
1469
|
+
|
|
1470
|
+
Does not send:
|
|
1471
|
+
The URL of the origin remote: it is compared on this machine.
|
|
1472
|
+
|
|
1473
|
+
Exit codes:
|
|
1474
|
+
0 The status was read, whether or not steps remain.
|
|
1475
|
+
1 Not logged in, the login expired, or communication failed.
|
|
1476
|
+
|
|
1477
|
+
Examples:
|
|
1478
|
+
harnessforce status
|
|
1479
|
+
harnessforce status --json
|
|
1448
1480
|
`
|
|
1449
1481
|
},
|
|
1450
1482
|
"otel-headers": {
|
|
@@ -1509,6 +1541,8 @@ Examples:
|
|
|
1509
1541
|
tune: {
|
|
1510
1542
|
summary: "Analyze your past sessions and report counts",
|
|
1511
1543
|
text: `Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json]
|
|
1544
|
+
harnessforce tune --report-only [--all]
|
|
1545
|
+
harnessforce tune --print-schedule
|
|
1512
1546
|
harnessforce tune record
|
|
1513
1547
|
harnessforce tune --purge
|
|
1514
1548
|
|
|
@@ -1528,6 +1562,11 @@ Options:
|
|
|
1528
1562
|
--show-report Print the exact values that would be sent.
|
|
1529
1563
|
--json Print the result as one JSON object on stdout. Messages go to
|
|
1530
1564
|
stderr.
|
|
1565
|
+
--report-only Analyze and send, writing nothing to stdout (for your OS
|
|
1566
|
+
scheduler). Combines with --all only.
|
|
1567
|
+
--print-schedule
|
|
1568
|
+
Print a cron line that runs --report-only every Monday at
|
|
1569
|
+
9:00. It changes nothing and sends nothing.
|
|
1531
1570
|
record Read one proposal as JSON from stdin and store it locally.
|
|
1532
1571
|
--purge Delete the analysis, proposals, and unsent results.
|
|
1533
1572
|
|
|
@@ -1549,6 +1588,7 @@ Exit codes:
|
|
|
1549
1588
|
Examples:
|
|
1550
1589
|
harnessforce tune
|
|
1551
1590
|
harnessforce tune --no-send --json
|
|
1591
|
+
harnessforce tune --print-schedule
|
|
1552
1592
|
harnessforce tune --purge
|
|
1553
1593
|
`
|
|
1554
1594
|
},
|
|
@@ -1623,6 +1663,7 @@ Examples:
|
|
|
1623
1663
|
const ORDER = [
|
|
1624
1664
|
"init",
|
|
1625
1665
|
"import",
|
|
1666
|
+
"status",
|
|
1626
1667
|
"run",
|
|
1627
1668
|
"tune",
|
|
1628
1669
|
"top",
|
|
@@ -2382,6 +2423,48 @@ async function fetchSessionImportDays(base, apiToken, fetchImpl) {
|
|
|
2382
2423
|
value: days
|
|
2383
2424
|
} : FAILED$1;
|
|
2384
2425
|
}
|
|
2426
|
+
const ROLES = /* @__PURE__ */ new Set([
|
|
2427
|
+
"owner",
|
|
2428
|
+
"admin",
|
|
2429
|
+
"member",
|
|
2430
|
+
"viewer"
|
|
2431
|
+
]);
|
|
2432
|
+
const STEP_STATUSES = /* @__PURE__ */ new Set([
|
|
2433
|
+
"done",
|
|
2434
|
+
"todo",
|
|
2435
|
+
"unavailable"
|
|
2436
|
+
]);
|
|
2437
|
+
function parseSteps(value) {
|
|
2438
|
+
if (!Array.isArray(value)) return void 0;
|
|
2439
|
+
const steps = value.map((item) => {
|
|
2440
|
+
if (typeof item !== "object" || item === null) return void 0;
|
|
2441
|
+
const { step, status, url } = item;
|
|
2442
|
+
return Number.isInteger(step) && step >= 2 && step <= 5 && STEP_STATUSES.has(status) && typeof url === "string" ? {
|
|
2443
|
+
step,
|
|
2444
|
+
status,
|
|
2445
|
+
url
|
|
2446
|
+
} : void 0;
|
|
2447
|
+
});
|
|
2448
|
+
return steps.every((step) => step !== void 0) ? steps : void 0;
|
|
2449
|
+
}
|
|
2450
|
+
async function fetchOnboarding(base, apiToken, fetchImpl) {
|
|
2451
|
+
const outcome = await getJson(underBase(base, "api/v1/onboarding"), apiToken, fetchImpl);
|
|
2452
|
+
if (outcome.kind !== "ok") return outcome;
|
|
2453
|
+
const body = outcome.value;
|
|
2454
|
+
const steps = parseSteps(body.steps);
|
|
2455
|
+
const adminSteps = body.admin_steps === void 0 ? void 0 : parseSteps(body.admin_steps);
|
|
2456
|
+
if (typeof body.workspace_name !== "string" || !ROLES.has(body.role) || typeof body.has_seat !== "boolean" || !steps || body.admin_steps !== void 0 && !adminSteps) return FAILED$1;
|
|
2457
|
+
return {
|
|
2458
|
+
kind: "ok",
|
|
2459
|
+
value: {
|
|
2460
|
+
workspace_name: body.workspace_name,
|
|
2461
|
+
role: body.role,
|
|
2462
|
+
has_seat: body.has_seat,
|
|
2463
|
+
steps,
|
|
2464
|
+
...adminSteps && { admin_steps: adminSteps }
|
|
2465
|
+
}
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2385
2468
|
// src/import/record-base.ts
|
|
2386
2469
|
async function readSettingsEnv(path) {
|
|
2387
2470
|
const read = await readUserSettings(path);
|
|
@@ -3254,7 +3337,7 @@ async function scanCodexSessions(options) {
|
|
|
3254
3337
|
}
|
|
3255
3338
|
// src/import/command.ts
|
|
3256
3339
|
const DAY_MS$1 = 864e5;
|
|
3257
|
-
const MESSAGES = {
|
|
3340
|
+
const MESSAGES$1 = {
|
|
3258
3341
|
runInit: "Run `harnessforce init`",
|
|
3259
3342
|
invalidUrl: "The connection URL is invalid",
|
|
3260
3343
|
keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
|
|
@@ -3271,15 +3354,15 @@ const limitMessage = {
|
|
|
3271
3354
|
monthly_event_limit: (count) => `Could not import ${count} session(s) because the monthly event limit was reached. Run \`harnessforce import\` again after the limit is lifted to resume importing`,
|
|
3272
3355
|
workspace_read_only: (count) => `Could not import ${count} session(s) because the Workspace is view-only. Run \`harnessforce import\` again after view-only mode is lifted to resume importing`
|
|
3273
3356
|
};
|
|
3274
|
-
const ACCESS_MESSAGES$
|
|
3275
|
-
keychainUnavailable: MESSAGES.keychainUnavailable,
|
|
3276
|
-
initRequired: MESSAGES.runInit,
|
|
3277
|
-
invalidUrl: MESSAGES.invalidUrl,
|
|
3278
|
-
apiTokenMissing: MESSAGES.runInit
|
|
3357
|
+
const ACCESS_MESSAGES$3 = {
|
|
3358
|
+
keychainUnavailable: MESSAGES$1.keychainUnavailable,
|
|
3359
|
+
initRequired: MESSAGES$1.runInit,
|
|
3360
|
+
invalidUrl: MESSAGES$1.invalidUrl,
|
|
3361
|
+
apiTokenMissing: MESSAGES$1.runInit
|
|
3279
3362
|
};
|
|
3280
|
-
function unwrap(outcome) {
|
|
3363
|
+
function unwrap$1(outcome) {
|
|
3281
3364
|
if (outcome.kind === "ok") return outcome.value;
|
|
3282
|
-
return stopWith(outcome.kind === "unauthorized" ? MESSAGES.loginExpired : MESSAGES.readFailed);
|
|
3365
|
+
return stopWith(outcome.kind === "unauthorized" ? MESSAGES$1.loginExpired : MESSAGES$1.readFailed);
|
|
3283
3366
|
}
|
|
3284
3367
|
const NOT_IMPORTED_LABELS = [
|
|
3285
3368
|
["alreadySent", "already sent"],
|
|
@@ -3295,8 +3378,8 @@ function reportNotImported(imported, notes, deps) {
|
|
|
3295
3378
|
const line = breakdownLine(notes.breakdown);
|
|
3296
3379
|
if (line) deps.stdout(`${line}\n`);
|
|
3297
3380
|
if (imported > 0) return;
|
|
3298
|
-
if (notes.breakdown.notConnected > 0) deps.stdout(`${notes.connectedCount === 0 ? MESSAGES.connectNone : MESSAGES.connectOther}\n`);
|
|
3299
|
-
else if (!line && !notes.otherNoticeShown) deps.stdout(`${MESSAGES.nothingFound}\n`);
|
|
3381
|
+
if (notes.breakdown.notConnected > 0) deps.stdout(`${notes.connectedCount === 0 ? MESSAGES$1.connectNone : MESSAGES$1.connectOther}\n`);
|
|
3382
|
+
else if (!line && !notes.otherNoticeShown) deps.stdout(`${MESSAGES$1.nothingFound}\n`);
|
|
3300
3383
|
}
|
|
3301
3384
|
function report(result, notes, deps) {
|
|
3302
3385
|
switch (result.kind) {
|
|
@@ -3312,10 +3395,10 @@ function report(result, notes, deps) {
|
|
|
3312
3395
|
deps.stderr(`${limitMessage[result.reason](result.notImported)}\n`);
|
|
3313
3396
|
return 1;
|
|
3314
3397
|
case "unauthorized":
|
|
3315
|
-
deps.stderr(`${MESSAGES.revoked}\n`);
|
|
3398
|
+
deps.stderr(`${MESSAGES$1.revoked}\n`);
|
|
3316
3399
|
return 1;
|
|
3317
3400
|
case "failed":
|
|
3318
|
-
deps.stderr(`${MESSAGES.sendFailed}\n`);
|
|
3401
|
+
deps.stderr(`${MESSAGES$1.sendFailed}\n`);
|
|
3319
3402
|
return 1;
|
|
3320
3403
|
}
|
|
3321
3404
|
}
|
|
@@ -3325,7 +3408,7 @@ const sumNotImported = (a, b) => ({
|
|
|
3325
3408
|
notConnected: a.notConnected + b.notConnected
|
|
3326
3409
|
});
|
|
3327
3410
|
async function runImport(deps) {
|
|
3328
|
-
const access = await verifyCliAccess(deps, ACCESS_MESSAGES$
|
|
3411
|
+
const access = await verifyCliAccess(deps, ACCESS_MESSAGES$3);
|
|
3329
3412
|
const destination = {
|
|
3330
3413
|
workspaceId: access.workspaceId,
|
|
3331
3414
|
endpoint: withoutExtras(access.ingest)
|
|
@@ -3339,8 +3422,8 @@ async function runImport(deps) {
|
|
|
3339
3422
|
sleep: deps.sleep,
|
|
3340
3423
|
homeDir: deps.homeDir
|
|
3341
3424
|
}).authorizedFetch(deps.fetch);
|
|
3342
|
-
const connected = unwrap(await listConnectedRepositories(access.readApiBase, access.accessToken, readFetch));
|
|
3343
|
-
const days = unwrap(await fetchSessionImportDays(access.readApiBase, access.accessToken, readFetch));
|
|
3425
|
+
const connected = unwrap$1(await listConnectedRepositories(access.readApiBase, access.accessToken, readFetch));
|
|
3426
|
+
const days = unwrap$1(await fetchSessionImportDays(access.readApiBase, access.accessToken, readFetch));
|
|
3344
3427
|
const sinceMs = deps.now() - days * DAY_MS$1;
|
|
3345
3428
|
const claude = await scanSessions({
|
|
3346
3429
|
projectsDir: join(await recordBase(deps), "projects"),
|
|
@@ -3369,7 +3452,7 @@ async function runImport(deps) {
|
|
|
3369
3452
|
sessions: unsent,
|
|
3370
3453
|
fetch: deps.fetch,
|
|
3371
3454
|
sleep: deps.sleep,
|
|
3372
|
-
record: (ids) => recordSentSessions(statePath, destination, ids).catch(() => stopWith(MESSAGES.stateFailed))
|
|
3455
|
+
record: (ids) => recordSentSessions(statePath, destination, ids).catch(() => stopWith(MESSAGES$1.stateFailed))
|
|
3373
3456
|
}), {
|
|
3374
3457
|
breakdown: {
|
|
3375
3458
|
alreadySent: scanned.length - unsent.length,
|
|
@@ -10579,14 +10662,14 @@ function candidateList(candidates) {
|
|
|
10579
10662
|
const lines = candidates.map(({ identifier, title }) => `${printable(identifier)} ${printable(title)}`);
|
|
10580
10663
|
return [RUN_MESSAGES.candidatesHeader, ...lines].join("\n");
|
|
10581
10664
|
}
|
|
10582
|
-
const ACCESS_MESSAGES$
|
|
10665
|
+
const ACCESS_MESSAGES$2 = {
|
|
10583
10666
|
keychainUnavailable: RUN_MESSAGES.keychainUnavailable,
|
|
10584
10667
|
initRequired: RUN_MESSAGES.initRequired,
|
|
10585
10668
|
invalidUrl: RUN_MESSAGES.invalidUrl,
|
|
10586
10669
|
apiTokenMissing: RUN_MESSAGES.issueInitRequired
|
|
10587
10670
|
};
|
|
10588
10671
|
async function prepareLaunch(args, deps) {
|
|
10589
|
-
const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$
|
|
10672
|
+
const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$2);
|
|
10590
10673
|
if (!isIssueIdentifier(args.issue)) stop("invalidIssue");
|
|
10591
10674
|
const readFetch = createApiTokenSession({
|
|
10592
10675
|
keychain: deps.keychain,
|
|
@@ -10651,6 +10734,87 @@ async function runWithoutHarnessforce(args, deps) {
|
|
|
10651
10734
|
deps.stderr(`${launchFailedMessage(args.agent)}\n`);
|
|
10652
10735
|
return 1;
|
|
10653
10736
|
}
|
|
10737
|
+
// src/status/command.ts
|
|
10738
|
+
const STATUS_USAGE = "Usage: harnessforce status [--json]\n";
|
|
10739
|
+
const MESSAGES = {
|
|
10740
|
+
failed: "Could not read the Harnessforce status. Run `harnessforce init` and try again.",
|
|
10741
|
+
loginExpired: "Your login has expired. Run `harnessforce init`"
|
|
10742
|
+
};
|
|
10743
|
+
const ACCESS_MESSAGES$1 = {
|
|
10744
|
+
keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
|
|
10745
|
+
initRequired: MESSAGES.failed,
|
|
10746
|
+
invalidUrl: MESSAGES.failed,
|
|
10747
|
+
apiTokenMissing: MESSAGES.failed
|
|
10748
|
+
};
|
|
10749
|
+
const STEP_TITLES = {
|
|
10750
|
+
2: "Connect the GitHub App",
|
|
10751
|
+
3: "Assign repositories to a project",
|
|
10752
|
+
4: "Set up telemetry",
|
|
10753
|
+
5: "Wait for your first run"
|
|
10754
|
+
};
|
|
10755
|
+
const STATUS_MARKS = {
|
|
10756
|
+
done: "✓",
|
|
10757
|
+
todo: "・",
|
|
10758
|
+
unavailable: "?"
|
|
10759
|
+
};
|
|
10760
|
+
function unwrap(outcome) {
|
|
10761
|
+
if (outcome.kind === "ok") return outcome.value;
|
|
10762
|
+
return stopWith(outcome.kind === "unauthorized" ? MESSAGES.loginExpired : MESSAGES.failed);
|
|
10763
|
+
}
|
|
10764
|
+
async function originRepository(cwd, git) {
|
|
10765
|
+
const url = await git(cwd, [
|
|
10766
|
+
"remote",
|
|
10767
|
+
"get-url",
|
|
10768
|
+
"origin"
|
|
10769
|
+
]);
|
|
10770
|
+
return url === void 0 ? void 0 : normalizeRepository(url);
|
|
10771
|
+
}
|
|
10772
|
+
function stepLine(step) {
|
|
10773
|
+
const parts = [STATUS_MARKS[step.status], STEP_TITLES[step.step]];
|
|
10774
|
+
if (step.status === "todo") parts.push(step.url);
|
|
10775
|
+
return parts.join(" ");
|
|
10776
|
+
}
|
|
10777
|
+
async function runStatus(json, deps) {
|
|
10778
|
+
const access = await verifyCliAccess(deps, ACCESS_MESSAGES$1);
|
|
10779
|
+
const readFetch = createApiTokenSession({
|
|
10780
|
+
keychain: deps.keychain,
|
|
10781
|
+
workspaceId: access.workspaceId,
|
|
10782
|
+
readBase: access.readApiBase,
|
|
10783
|
+
fetch: deps.fetch,
|
|
10784
|
+
now: deps.now,
|
|
10785
|
+
sleep: deps.sleep,
|
|
10786
|
+
homeDir: deps.homeDir
|
|
10787
|
+
}).authorizedFetch(deps.fetch);
|
|
10788
|
+
const onboarding = unwrap(await fetchOnboarding(access.readApiBase, access.accessToken, readFetch));
|
|
10789
|
+
const name = await originRepository(deps.cwd, deps.git);
|
|
10790
|
+
const repository = name === void 0 ? null : {
|
|
10791
|
+
name,
|
|
10792
|
+
connected: unwrap(await listConnectedRepositories(access.readApiBase, access.accessToken, readFetch)).has(name)
|
|
10793
|
+
};
|
|
10794
|
+
if (json) {
|
|
10795
|
+
deps.stdout(`${JSON.stringify({
|
|
10796
|
+
...onboarding,
|
|
10797
|
+
repository
|
|
10798
|
+
})}\n`);
|
|
10799
|
+
return 0;
|
|
10800
|
+
}
|
|
10801
|
+
const lines = [...onboarding.admin_steps ?? [], ...onboarding.steps].sort((a, b) => a.step - b.step).map(stepLine);
|
|
10802
|
+
const repositoryLine = repository && `This repository (${repository.name}) is ${repository.connected ? "connected" : "not connected"} to the GitHub App`;
|
|
10803
|
+
deps.stdout(`${[
|
|
10804
|
+
`${onboarding.workspace_name} (${onboarding.role})`,
|
|
10805
|
+
...lines,
|
|
10806
|
+
...repositoryLine ? [repositoryLine] : []
|
|
10807
|
+
].join("\n")}\n`);
|
|
10808
|
+
return 0;
|
|
10809
|
+
}
|
|
10810
|
+
function statusCommand(args, deps) {
|
|
10811
|
+
const json = args.length === 1 && args[0] === "--json";
|
|
10812
|
+
if (args.length > 0 && !json) {
|
|
10813
|
+
deps.stderr(STATUS_USAGE);
|
|
10814
|
+
return Promise.resolve(1);
|
|
10815
|
+
}
|
|
10816
|
+
return runUntilStop(() => runStatus(json, deps), deps.stderr);
|
|
10817
|
+
}
|
|
10654
10818
|
// src/top/args.ts
|
|
10655
10819
|
const TOP_USAGE = "Usage: harnessforce top [--once] [--json] [--ascii] [--theme <auto|light|dark|ansi>]\n";
|
|
10656
10820
|
const THEMES = [
|
|
@@ -12284,7 +12448,7 @@ const TUNE_MESSAGES = {
|
|
|
12284
12448
|
unwritable: "Cannot write to `~/.harnessforce/tune/`, so the analysis results are not sent",
|
|
12285
12449
|
recordFailed: "Could not record the suggestion",
|
|
12286
12450
|
recordInvalid: (field) => `The suggestion record input is invalid: ${field}`,
|
|
12287
|
-
usage: "Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json], harnessforce tune record, or harnessforce tune --purge",
|
|
12451
|
+
usage: "Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json], harnessforce tune --report-only [--all], harnessforce tune --print-schedule, harnessforce tune record, or harnessforce tune --purge",
|
|
12288
12452
|
purged: "Deleted the analysis results, suggestions and unsent analysis results in `~/.harnessforce/tune/`",
|
|
12289
12453
|
purgeFailed: "Could not delete some files in `~/.harnessforce/tune/`"
|
|
12290
12454
|
};
|
|
@@ -12870,7 +13034,7 @@ async function analyzeUnderLock(options, access, paths, deps) {
|
|
|
12870
13034
|
]).then(() => true, () => false);
|
|
12871
13035
|
if (!isWritten) notices.add("unwritable", TUNE_MESSAGES.unwritable, 1);
|
|
12872
13036
|
if (options.json) deps.stdout(`${asciiJson(output)}\n`);
|
|
12873
|
-
else {
|
|
13037
|
+
else if (!options.reportOnly) {
|
|
12874
13038
|
deps.stdout(renderAnalysis(analyzed.map((a) => a.report), analyzed.map((a) => a.tune.usage), {
|
|
12875
13039
|
sessionCount: analyzed.length,
|
|
12876
13040
|
analyzerVersion: ANALYZER_VERSION,
|
|
@@ -13110,27 +13274,36 @@ const FLAGS = {
|
|
|
13110
13274
|
"--all": "all",
|
|
13111
13275
|
"--no-send": "noSend",
|
|
13112
13276
|
"--show-report": "showReport",
|
|
13113
|
-
"--json": "json"
|
|
13277
|
+
"--json": "json",
|
|
13278
|
+
"--report-only": "reportOnly"
|
|
13114
13279
|
};
|
|
13280
|
+
const REPORT_ONLY = "--report-only";
|
|
13115
13281
|
function parseArgs(args) {
|
|
13116
13282
|
if (args.length === 1 && args[0] === "record") return { kind: "record" };
|
|
13117
13283
|
if (args.length === 1 && args[0] === "--purge") return { kind: "purge" };
|
|
13284
|
+
if (args.length === 1 && args[0] === "--print-schedule") return { kind: "printSchedule" };
|
|
13118
13285
|
const options = {
|
|
13119
13286
|
all: false,
|
|
13120
13287
|
noSend: false,
|
|
13121
13288
|
showReport: false,
|
|
13122
|
-
json: false
|
|
13289
|
+
json: false,
|
|
13290
|
+
reportOnly: false
|
|
13123
13291
|
};
|
|
13124
13292
|
for (const arg of args) {
|
|
13125
13293
|
const key = FLAGS[arg];
|
|
13126
13294
|
if (key === void 0 || options[key]) return void 0;
|
|
13127
13295
|
options[key] = true;
|
|
13128
13296
|
}
|
|
13297
|
+
if (options.reportOnly && (options.noSend || options.showReport || options.json)) return void 0;
|
|
13129
13298
|
return {
|
|
13130
13299
|
kind: "analyze",
|
|
13131
13300
|
options
|
|
13132
13301
|
};
|
|
13133
13302
|
}
|
|
13303
|
+
function cronCommandPath(path) {
|
|
13304
|
+
return (/^[A-Za-z0-9_./-]+$/.test(path) ? path : `'${path.replaceAll("'", "'\\''")}'`).replaceAll("%", "\\%");
|
|
13305
|
+
}
|
|
13306
|
+
const scheduleLine = (selfPath) => `0 9 * * 1 ${cronCommandPath(selfPath)} tune ${REPORT_ONLY}\n`;
|
|
13134
13307
|
function canTuneSend(args) {
|
|
13135
13308
|
const parsed = parseArgs(args);
|
|
13136
13309
|
return parsed?.kind === "analyze" && !parsed.options.noSend;
|
|
@@ -13180,6 +13353,10 @@ async function tuneCommand(args, deps) {
|
|
|
13180
13353
|
return 2;
|
|
13181
13354
|
}
|
|
13182
13355
|
if (parsed.kind === "record") return record(deps);
|
|
13356
|
+
if (parsed.kind === "printSchedule") {
|
|
13357
|
+
deps.stdout(scheduleLine(deps.selfPath));
|
|
13358
|
+
return 0;
|
|
13359
|
+
}
|
|
13183
13360
|
if (parsed.kind === "purge") return underLock(deps, TUNE_MESSAGES.purgeFailed, async (paths) => {
|
|
13184
13361
|
if (!await purgeTuneFiles(paths)) return stopWith(TUNE_MESSAGES.purgeFailed);
|
|
13185
13362
|
deps.stderr(`${TUNE_MESSAGES.purged}\n`);
|
|
@@ -13284,6 +13461,11 @@ async function run(argv, deps) {
|
|
|
13284
13461
|
git: cli.importGit,
|
|
13285
13462
|
now: () => cli.now().getTime()
|
|
13286
13463
|
}));
|
|
13464
|
+
if (command === "status") return guarded(() => statusCommand(rest, {
|
|
13465
|
+
...cli,
|
|
13466
|
+
git: cli.importGit,
|
|
13467
|
+
now: () => cli.now().getTime()
|
|
13468
|
+
}));
|
|
13287
13469
|
if (command === "tune") return guarded(() => tuneCommand(rest, {
|
|
13288
13470
|
...cli,
|
|
13289
13471
|
git: cli.importGit,
|
|
@@ -13341,6 +13523,7 @@ const SENDING_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
13341
13523
|
"init",
|
|
13342
13524
|
"run",
|
|
13343
13525
|
"import",
|
|
13526
|
+
"status",
|
|
13344
13527
|
"hook"
|
|
13345
13528
|
]);
|
|
13346
13529
|
const isSending = (argv) => SENDING_COMMANDS.has(argv[0] ?? "") || argv[0] === "tune" && canTuneSend(argv.slice(1));
|
|
@@ -13543,6 +13726,7 @@ process.exitCode = relaunched ?? await run(argv, {
|
|
|
13543
13726
|
devBuild: !isDefault,
|
|
13544
13727
|
callbackTimeoutMs: CALLBACK_TIMEOUT_MS,
|
|
13545
13728
|
cwd: process.cwd(),
|
|
13729
|
+
selfPath: process.argv[1],
|
|
13546
13730
|
git: createGitRunner({
|
|
13547
13731
|
platform: process.platform,
|
|
13548
13732
|
env: process.env,
|