wood-fired-tasks 2.0.0 → 2.0.2
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 +36 -0
- package/dist/cli/commands/setup.d.ts +28 -15
- package/dist/cli/commands/setup.js +46 -23
- package/dist/db/migrate.d.ts +6 -2
- package/dist/db/migrate.js +9 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,42 @@ vulnerabilities, supply-chain pinning) are always called out under `Security`.
|
|
|
13
13
|
|
|
14
14
|
_No changes yet._
|
|
15
15
|
|
|
16
|
+
## [v2.0.2] - 2026-06-08
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **`tasks setup` remote onboarding now reaches the automated device-flow login
|
|
20
|
+
instead of always falling back to manual PAT entry.** The onboarding probe
|
|
21
|
+
read the server's OIDC state from `GET /health/detailed`, but v2.0's identity
|
|
22
|
+
cutover put that endpoint behind Bearer-PAT auth — so an unauthenticated probe
|
|
23
|
+
(the only kind possible *before* you have a token) always got `401`, the OIDC
|
|
24
|
+
state was "undeterminable", and setup fell back to asking you to paste a
|
|
25
|
+
personal access token. The probe now targets the **public** `GET /auth/login`
|
|
26
|
+
endpoint (`redirect: 'manual'`): a `3xx` redirect to the IdP means browser
|
|
27
|
+
login is available (`ready`), a `501` means OIDC is disabled (`manual-pat`).
|
|
28
|
+
Verified against a live OIDC-ready server: the probe now resolves `ready`
|
|
29
|
+
where it previously 401'd, so the device-flow browser login launches as
|
|
30
|
+
intended.
|
|
31
|
+
|
|
32
|
+
## [v2.0.1] - 2026-06-08
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- **`tasks setup` interactive "Remote" selection no longer crashes.** Choosing
|
|
36
|
+
option **3) Remote** from the interactive setup menu threw
|
|
37
|
+
`remote onboarding requires a --remote <url> base URL.` because the menu set
|
|
38
|
+
the mode but never captured the server URL — only the `--remote <url>` flag
|
|
39
|
+
did. The interactive remote path now prompts for the base URL when it is not
|
|
40
|
+
supplied as a flag, guarded by a TTY check so non-interactive/CI callers still
|
|
41
|
+
fail fast with the same clear message instead of hanging on stdin.
|
|
42
|
+
- **DB-path legacy-adopt tests are now hermetic.** `migrate-db-path.test.ts` and
|
|
43
|
+
`config-path-e2e.test.ts` exercise the "adopt `./data/tasks.db` when the OS
|
|
44
|
+
app-data DB is absent" precedence, but probed the real filesystem and so
|
|
45
|
+
failed on any dev machine where the `tasks` CLI had already created
|
|
46
|
+
`~/.local/share/wood-fired-tasks/tasks.db` (they passed on clean CI by luck).
|
|
47
|
+
`resolveMigrateDbPath`/`migrateCli` now forward the resolver's existing
|
|
48
|
+
injectable `exists` seam (default `fs.existsSync`, no behavior change), and the
|
|
49
|
+
tests inject a probe that hides the app-data DB — making the precedence-2
|
|
50
|
+
cases deterministic regardless of the developer's local state.
|
|
51
|
+
|
|
16
52
|
## [v2.0.0] - 2026-06-07
|
|
17
53
|
|
|
18
54
|
The **identity auth cutover** — a breaking major. The legacy `X-API-Key`
|
|
@@ -274,9 +274,11 @@ export declare function writeRemoteMcpEntryOnly(options?: RunSetupOptions): RunS
|
|
|
274
274
|
*/
|
|
275
275
|
export type SetupMode = 'local' | 'service' | 'remote';
|
|
276
276
|
/**
|
|
277
|
-
* Coarse OIDC subsystem state
|
|
278
|
-
*
|
|
279
|
-
* (
|
|
277
|
+
* Coarse OIDC subsystem state used by the remote-setup branch selector (#807)
|
|
278
|
+
* to route deterministically. Derived by {@link probeOidcState} from the public
|
|
279
|
+
* `/auth/login` endpoint (`ready` = redirects to IdP, `disabled` = 501 stub).
|
|
280
|
+
* `degraded` is retained for back-compat but is no longer emitted by the probe
|
|
281
|
+
* (an unhealthy OIDC surfaces as an inconclusive status → manual-PAT fallback).
|
|
280
282
|
*/
|
|
281
283
|
export type OidcState = 'ready' | 'disabled' | 'degraded';
|
|
282
284
|
/**
|
|
@@ -287,10 +289,12 @@ export type OidcState = 'ready' | 'disabled' | 'degraded';
|
|
|
287
289
|
*/
|
|
288
290
|
export type RemoteOnboardingMethod = 'device-flow' | 'manual-pat';
|
|
289
291
|
/**
|
|
290
|
-
* Result of probing
|
|
291
|
-
*
|
|
292
|
-
* - `{ ok:
|
|
293
|
-
*
|
|
292
|
+
* Result of probing the server for whether browser/device-flow login is
|
|
293
|
+
* available.
|
|
294
|
+
* - `{ ok: true, oidc }` — the probe got a conclusive answer (`ready` =
|
|
295
|
+
* device-flow available, `disabled` = no OIDC → manual PAT).
|
|
296
|
+
* - `{ ok: false, reason }` — network error / inconclusive status. The
|
|
297
|
+
* `reason` is surfaced to the user before falling back to manual PAT.
|
|
294
298
|
*/
|
|
295
299
|
export type OidcProbeResult = {
|
|
296
300
|
ok: true;
|
|
@@ -302,13 +306,22 @@ export type OidcProbeResult = {
|
|
|
302
306
|
/** Injectable probe signature so tests can drive each branch without a server. */
|
|
303
307
|
export type OidcProbe = (baseUrl: string) => Promise<OidcProbeResult>;
|
|
304
308
|
/**
|
|
305
|
-
* Default OIDC probe: `GET <baseUrl>/
|
|
309
|
+
* Default OIDC probe: `GET <baseUrl>/auth/login` (the browser-login entry
|
|
310
|
+
* point) and read OIDC availability from the HTTP status.
|
|
306
311
|
*
|
|
307
|
-
* `/health/detailed`
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
+
* Why NOT `/health/detailed` (the original #807 target): v2.0's identity
|
|
313
|
+
* cutover put `/health/detailed` behind Bearer-PAT auth, so an unauthenticated
|
|
314
|
+
* probe — which is the ONLY kind possible during onboarding, before any PAT
|
|
315
|
+
* exists — gets a hard `401 {"error":"UNAUTHORIZED"}` with NO `oidc.state` in
|
|
316
|
+
* the body. That made this probe always fail and `setup --remote` always fall
|
|
317
|
+
* back to manual-PAT entry, defeating the automated device-flow login
|
|
318
|
+
* (wood-fired-tasks #831).
|
|
319
|
+
*
|
|
320
|
+
* `/auth/login` is PUBLIC (`skipAuth: true`) and an unambiguous signal:
|
|
321
|
+
* - OIDC ready → `302` redirect to the IdP authorize URL.
|
|
322
|
+
* - OIDC disabled → the disabled-stub returns `501`.
|
|
323
|
+
* We do NOT follow the redirect (`redirect: 'manual'`) — only its status
|
|
324
|
+
* matters. Any other/unreachable status is inconclusive → manual-PAT fallback.
|
|
312
325
|
* Bounded by a 5s timeout so a half-open server can't hang `setup --remote`.
|
|
313
326
|
*/
|
|
314
327
|
export declare function probeOidcState(baseUrl: string): Promise<OidcProbeResult>;
|
|
@@ -367,8 +380,8 @@ export declare function persistManualPat(baseUrl: string, token: string): Promis
|
|
|
367
380
|
export interface RunSetupInteractiveOptions extends RunSetupOptions {
|
|
368
381
|
/**
|
|
369
382
|
* Injectable OIDC-state probe for the `--remote` path (#807). Defaults to
|
|
370
|
-
* {@link probeOidcState} (a real `GET /
|
|
371
|
-
* drive the ready / disabled /
|
|
383
|
+
* {@link probeOidcState} (a real `GET /auth/login`, public). Tests stub this
|
|
384
|
+
* to drive the ready / disabled / probe-failure branches.
|
|
372
385
|
*/
|
|
373
386
|
oidcProbe?: OidcProbe;
|
|
374
387
|
/**
|
|
@@ -8,7 +8,7 @@ import { resolveAssetPath } from '../../assets/resolve.js';
|
|
|
8
8
|
import { configDir as defaultConfigDir } from '../../config/paths.js';
|
|
9
9
|
import { resolvePathHint } from '../util/path-hint.js';
|
|
10
10
|
import { buildNpmInvocation } from '../util/npm-spawn.js';
|
|
11
|
-
import { selectFromMenu, promptSecret } from '../util/prompt.js';
|
|
11
|
+
import { selectFromMenu, promptSecret, promptLine } from '../util/prompt.js';
|
|
12
12
|
import { shouldPrompt } from '../prompts/interactive.js';
|
|
13
13
|
import { getServiceBackend } from './service.js';
|
|
14
14
|
import { runDeviceLogin } from './login.js';
|
|
@@ -546,40 +546,50 @@ export function writeRemoteMcpEntryOnly(options = {}) {
|
|
|
546
546
|
};
|
|
547
547
|
}
|
|
548
548
|
/**
|
|
549
|
-
* Default OIDC probe: `GET <baseUrl>/
|
|
549
|
+
* Default OIDC probe: `GET <baseUrl>/auth/login` (the browser-login entry
|
|
550
|
+
* point) and read OIDC availability from the HTTP status.
|
|
550
551
|
*
|
|
551
|
-
* `/health/detailed`
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
552
|
+
* Why NOT `/health/detailed` (the original #807 target): v2.0's identity
|
|
553
|
+
* cutover put `/health/detailed` behind Bearer-PAT auth, so an unauthenticated
|
|
554
|
+
* probe — which is the ONLY kind possible during onboarding, before any PAT
|
|
555
|
+
* exists — gets a hard `401 {"error":"UNAUTHORIZED"}` with NO `oidc.state` in
|
|
556
|
+
* the body. That made this probe always fail and `setup --remote` always fall
|
|
557
|
+
* back to manual-PAT entry, defeating the automated device-flow login
|
|
558
|
+
* (wood-fired-tasks #831).
|
|
559
|
+
*
|
|
560
|
+
* `/auth/login` is PUBLIC (`skipAuth: true`) and an unambiguous signal:
|
|
561
|
+
* - OIDC ready → `302` redirect to the IdP authorize URL.
|
|
562
|
+
* - OIDC disabled → the disabled-stub returns `501`.
|
|
563
|
+
* We do NOT follow the redirect (`redirect: 'manual'`) — only its status
|
|
564
|
+
* matters. Any other/unreachable status is inconclusive → manual-PAT fallback.
|
|
556
565
|
* Bounded by a 5s timeout so a half-open server can't hang `setup --remote`.
|
|
557
566
|
*/
|
|
558
567
|
export async function probeOidcState(baseUrl) {
|
|
559
|
-
const probeUrl = new URL('/
|
|
568
|
+
const probeUrl = new URL('/auth/login', baseUrl).toString();
|
|
560
569
|
let response;
|
|
561
570
|
try {
|
|
562
|
-
response = await fetch(probeUrl, {
|
|
571
|
+
response = await fetch(probeUrl, {
|
|
572
|
+
method: 'GET',
|
|
573
|
+
redirect: 'manual',
|
|
574
|
+
signal: AbortSignal.timeout(5000),
|
|
575
|
+
});
|
|
563
576
|
}
|
|
564
577
|
catch (err) {
|
|
565
578
|
const message = err instanceof Error ? err.message : String(err);
|
|
566
579
|
return { ok: false, reason: `could not reach ${probeUrl}: ${message}` };
|
|
567
580
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
catch {
|
|
576
|
-
return { ok: false, reason: `${probeUrl} returned a non-JSON body` };
|
|
581
|
+
// 3xx → /auth/login is redirecting to the IdP → device-flow is available.
|
|
582
|
+
// (`redirect: 'manual'` surfaces the real 3xx status rather than following
|
|
583
|
+
// it; some runtimes report an opaque-redirect as status 0 with type
|
|
584
|
+
// 'opaqueredirect', which we also treat as a redirect.)
|
|
585
|
+
if ((response.status >= 300 && response.status < 400) || response.type === 'opaqueredirect') {
|
|
586
|
+
return { ok: true, oidc: 'ready' };
|
|
577
587
|
}
|
|
578
|
-
|
|
579
|
-
if (
|
|
580
|
-
return { ok: true, oidc };
|
|
588
|
+
// 501 → the OIDC-disabled stub → no browser login; use a manual PAT.
|
|
589
|
+
if (response.status === 501) {
|
|
590
|
+
return { ok: true, oidc: 'disabled' };
|
|
581
591
|
}
|
|
582
|
-
return { ok: false, reason: `${probeUrl}
|
|
592
|
+
return { ok: false, reason: `${probeUrl} returned HTTP ${response.status}` };
|
|
583
593
|
}
|
|
584
594
|
/**
|
|
585
595
|
* Map a probe outcome to the onboarding method (task #807).
|
|
@@ -719,7 +729,20 @@ export async function runSetupInteractive(options = {}) {
|
|
|
719
729
|
// token} without a URL can't get a LOCAL install mislabeled as remote.
|
|
720
730
|
if (mode === 'remote') {
|
|
721
731
|
const hasToken = typeof options.token === 'string' && options.token.length > 0;
|
|
722
|
-
|
|
732
|
+
let hasRemote = typeof options.remote === 'string' && options.remote.length > 0;
|
|
733
|
+
// Picking "Remote" from the interactive menu sets `mode` but NEVER the base
|
|
734
|
+
// URL — only the `--remote <url>` flag populates `options.remote`. Without
|
|
735
|
+
// this prompt the menu selection falls straight into runRemoteOnboarding,
|
|
736
|
+
// which throws 'remote onboarding requires a --remote <url> base URL.'.
|
|
737
|
+
// Prompt for it here so the menu path is usable; guarded by isInteractive()
|
|
738
|
+
// so a non-TTY / programmatic caller never hangs waiting on stdin.
|
|
739
|
+
if (!hasRemote && isInteractive()) {
|
|
740
|
+
const url = (await promptLine('Remote server base URL (e.g. http://host:3000): ', options.promptIO)).trim();
|
|
741
|
+
if (url.length > 0) {
|
|
742
|
+
options = { ...options, remote: url };
|
|
743
|
+
hasRemote = true;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
723
746
|
if (hasToken && hasRemote) {
|
|
724
747
|
const setup = runSetup(options);
|
|
725
748
|
return { mode: 'remote', oidc: null, method: 'manual-pat', ok: true, setup };
|
package/dist/db/migrate.d.ts
CHANGED
|
@@ -23,8 +23,12 @@ export declare function runMigrations(db: Database.Database): Promise<void>;
|
|
|
23
23
|
* @param env - environment map (defaults to `process.env`).
|
|
24
24
|
* @param cwd - base directory for resolving relative paths and probing the
|
|
25
25
|
* legacy `./data/tasks.db` (defaults to `process.cwd()`).
|
|
26
|
+
* @param exists - filesystem existence probe forwarded to the unified resolver
|
|
27
|
+
* (defaults to `fs.existsSync`). Injectable so tests can isolate the
|
|
28
|
+
* legacy-adopt vs app-data branches without depending on whether the real OS
|
|
29
|
+
* app-data DB happens to exist on the dev machine running the suite.
|
|
26
30
|
*/
|
|
27
|
-
export declare function resolveMigrateDbPath(env?: NodeJS.ProcessEnv, cwd?: string): string;
|
|
31
|
+
export declare function resolveMigrateDbPath(env?: NodeJS.ProcessEnv, cwd?: string, exists?: (p: string) => boolean): string;
|
|
28
32
|
/**
|
|
29
33
|
* CLI entry point body: resolve the target DB path via the unified resolver
|
|
30
34
|
* (env > legacy-adopt > app-data default), ensure the parent
|
|
@@ -32,4 +36,4 @@ export declare function resolveMigrateDbPath(env?: NodeJS.ProcessEnv, cwd?: stri
|
|
|
32
36
|
*
|
|
33
37
|
* Extracted from the `isMain` guard so it is directly unit-testable.
|
|
34
38
|
*/
|
|
35
|
-
export declare function migrateCli(env?: NodeJS.ProcessEnv, cwd?: string): Promise<string>;
|
|
39
|
+
export declare function migrateCli(env?: NodeJS.ProcessEnv, cwd?: string, exists?: (p: string) => boolean): Promise<string>;
|
package/dist/db/migrate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Umzug } from 'umzug';
|
|
2
2
|
import { mkdir } from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
3
4
|
import { dirname, resolve, sep } from 'path';
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
5
6
|
import { initDatabase } from './database.js';
|
|
@@ -142,9 +143,13 @@ export async function runMigrations(db) {
|
|
|
142
143
|
* @param env - environment map (defaults to `process.env`).
|
|
143
144
|
* @param cwd - base directory for resolving relative paths and probing the
|
|
144
145
|
* legacy `./data/tasks.db` (defaults to `process.cwd()`).
|
|
146
|
+
* @param exists - filesystem existence probe forwarded to the unified resolver
|
|
147
|
+
* (defaults to `fs.existsSync`). Injectable so tests can isolate the
|
|
148
|
+
* legacy-adopt vs app-data branches without depending on whether the real OS
|
|
149
|
+
* app-data DB happens to exist on the dev machine running the suite.
|
|
145
150
|
*/
|
|
146
|
-
export function resolveMigrateDbPath(env = process.env, cwd = process.cwd()) {
|
|
147
|
-
return resolve(cwd, resolveDbPath(env, cwd));
|
|
151
|
+
export function resolveMigrateDbPath(env = process.env, cwd = process.cwd(), exists = existsSync) {
|
|
152
|
+
return resolve(cwd, resolveDbPath(env, cwd, exists));
|
|
148
153
|
}
|
|
149
154
|
/**
|
|
150
155
|
* CLI entry point body: resolve the target DB path via the unified resolver
|
|
@@ -153,8 +158,8 @@ export function resolveMigrateDbPath(env = process.env, cwd = process.cwd()) {
|
|
|
153
158
|
*
|
|
154
159
|
* Extracted from the `isMain` guard so it is directly unit-testable.
|
|
155
160
|
*/
|
|
156
|
-
export async function migrateCli(env = process.env, cwd = process.cwd()) {
|
|
157
|
-
const dbPath = resolveMigrateDbPath(env, cwd);
|
|
161
|
+
export async function migrateCli(env = process.env, cwd = process.cwd(), exists = existsSync) {
|
|
162
|
+
const dbPath = resolveMigrateDbPath(env, cwd, exists);
|
|
158
163
|
// Create the parent directory (e.g. ./data, or a user-supplied path) if
|
|
159
164
|
// it doesn't already exist. `recursive: true` is a no-op when present.
|
|
160
165
|
await mkdir(dirname(dbPath), { recursive: true });
|