@alessandroraffa/tangyr 0.11.1 → 0.12.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 +14 -0
- package/dist/index.js +530 -58
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@ CLI for the Tangyr discipline — install and manage operating kits for AI codin
|
|
|
4
4
|
|
|
5
5
|
An operating kit is a self-contained package of agents, skills, commands, rules, and templates that implements the Tangyr discipline for a specific AI coding context. The CLI places kit artifacts into the environments consumed by supported tools.
|
|
6
6
|
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
Node.js `>=22`. Running `tangyr` on an older Node.js version prints a clear error to stderr and exits without attempting to run any command.
|
|
10
|
+
|
|
7
11
|
## Install / run
|
|
8
12
|
|
|
9
13
|
```sh
|
|
@@ -49,6 +53,16 @@ tangyr <command>
|
|
|
49
53
|
| `tangyr config` | Display or edit Tangyr configuration |
|
|
50
54
|
| `tangyr validate` | Validate the kit against kitFormat 1 conformity rules |
|
|
51
55
|
|
|
56
|
+
### Authentication
|
|
57
|
+
|
|
58
|
+
| Command | Description |
|
|
59
|
+
| -------------------- | --------------------------------------------------------------- |
|
|
60
|
+
| `tangyr auth login` | Store a client ID/secret credential pair, validating by default |
|
|
61
|
+
| `tangyr auth status` | Show current credential validation state |
|
|
62
|
+
| `tangyr auth logout` | Remove the stored credential |
|
|
63
|
+
|
|
64
|
+
Resolved environment-first (`TANGYR_ACCESS_CLIENT_ID`/`TANGYR_ACCESS_CLIENT_SECRET`), then from the stored file, then interactively. A full quickstart (configuring a remote kit origin and installing against it) lands once remote kit resolution ships.
|
|
65
|
+
|
|
52
66
|
## Global flags
|
|
53
67
|
|
|
54
68
|
| Flag | Description |
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,11 @@ var __require = /* @__PURE__ */ ((x2) => typeof require !== "undefined" ? requir
|
|
|
14
14
|
throw Error('Dynamic require of "' + x2 + '" is not supported');
|
|
15
15
|
});
|
|
16
16
|
var __commonJS = (cb, mod) => function __require2() {
|
|
17
|
-
|
|
17
|
+
try {
|
|
18
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
throw mod = 0, e;
|
|
21
|
+
}
|
|
18
22
|
};
|
|
19
23
|
var __copyProps = (to, from, except, desc) => {
|
|
20
24
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
@@ -14761,6 +14765,60 @@ var dist_default6 = createPrompt((config, done) => {
|
|
|
14761
14765
|
];
|
|
14762
14766
|
});
|
|
14763
14767
|
|
|
14768
|
+
// node_modules/@inquirer/password/dist/index.js
|
|
14769
|
+
var passwordTheme = {
|
|
14770
|
+
style: {
|
|
14771
|
+
maskedText: "[input is masked]"
|
|
14772
|
+
}
|
|
14773
|
+
};
|
|
14774
|
+
var dist_default7 = createPrompt((config, done) => {
|
|
14775
|
+
const { validate = () => true } = config;
|
|
14776
|
+
const theme = makeTheme(passwordTheme, config.theme);
|
|
14777
|
+
const [status, setStatus] = useState("idle");
|
|
14778
|
+
const [errorMsg, setError] = useState();
|
|
14779
|
+
const [value, setValue] = useState("");
|
|
14780
|
+
const prefix = usePrefix({ status, theme });
|
|
14781
|
+
useKeypress(async (key, rl) => {
|
|
14782
|
+
if (status !== "idle") {
|
|
14783
|
+
return;
|
|
14784
|
+
}
|
|
14785
|
+
if (isEnterKey(key)) {
|
|
14786
|
+
const answer = value;
|
|
14787
|
+
setStatus("loading");
|
|
14788
|
+
const isValid = await validate(answer);
|
|
14789
|
+
if (isValid === true) {
|
|
14790
|
+
setValue(answer);
|
|
14791
|
+
setStatus("done");
|
|
14792
|
+
done(answer);
|
|
14793
|
+
} else {
|
|
14794
|
+
rl.write(value);
|
|
14795
|
+
setError(isValid || "You must provide a valid value");
|
|
14796
|
+
setStatus("idle");
|
|
14797
|
+
}
|
|
14798
|
+
} else {
|
|
14799
|
+
setValue(rl.line);
|
|
14800
|
+
setError(void 0);
|
|
14801
|
+
}
|
|
14802
|
+
});
|
|
14803
|
+
const message = theme.style.message(config.message, status);
|
|
14804
|
+
let formattedValue = "";
|
|
14805
|
+
let helpTip;
|
|
14806
|
+
if (config.mask) {
|
|
14807
|
+
const maskChar = typeof config.mask === "string" ? config.mask : "*";
|
|
14808
|
+
formattedValue = maskChar.repeat(value.length);
|
|
14809
|
+
} else if (status !== "done") {
|
|
14810
|
+
helpTip = `${theme.style.help(theme.style.maskedText)}${cursorHide}`;
|
|
14811
|
+
}
|
|
14812
|
+
if (status === "done") {
|
|
14813
|
+
formattedValue = theme.style.answer(formattedValue);
|
|
14814
|
+
}
|
|
14815
|
+
let error = "";
|
|
14816
|
+
if (errorMsg) {
|
|
14817
|
+
error = theme.style.error(errorMsg);
|
|
14818
|
+
}
|
|
14819
|
+
return [[prefix, message, config.mask ? formattedValue : helpTip].join(" "), error];
|
|
14820
|
+
});
|
|
14821
|
+
|
|
14764
14822
|
// node_modules/@inquirer/select/dist/index.js
|
|
14765
14823
|
import { styleText as styleText4 } from "util";
|
|
14766
14824
|
var selectTheme = {
|
|
@@ -14806,7 +14864,7 @@ function normalizeChoices2(choices) {
|
|
|
14806
14864
|
return normalizedChoice;
|
|
14807
14865
|
});
|
|
14808
14866
|
}
|
|
14809
|
-
var
|
|
14867
|
+
var dist_default8 = createPrompt((config, done) => {
|
|
14810
14868
|
const { loop = true, pageSize = 7 } = config;
|
|
14811
14869
|
const theme = makeTheme(selectTheme, config.theme);
|
|
14812
14870
|
const { keybindings } = theme;
|
|
@@ -15093,9 +15151,12 @@ function getPackageRoot(importMetaUrl) {
|
|
|
15093
15151
|
current = parent;
|
|
15094
15152
|
}
|
|
15095
15153
|
}
|
|
15154
|
+
function computeBytesHash(content) {
|
|
15155
|
+
return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
|
|
15156
|
+
}
|
|
15096
15157
|
function computeSourceHash(filePath) {
|
|
15097
15158
|
const content = fs2.readFileSync(filePath);
|
|
15098
|
-
return
|
|
15159
|
+
return computeBytesHash(content);
|
|
15099
15160
|
}
|
|
15100
15161
|
function formatProvenanceMarker(format, source, hash, target) {
|
|
15101
15162
|
if (format === "markdown") {
|
|
@@ -15404,6 +15465,11 @@ var EXIT_ERROR_SEVERITY_LOSS = 3;
|
|
|
15404
15465
|
var EXIT_TARGET_NOT_DETECTED = 4;
|
|
15405
15466
|
var EXIT_CONFIG_MISSING = 10;
|
|
15406
15467
|
var EXIT_USER_INTERRUPT = 130;
|
|
15468
|
+
var EXIT_AUTH_REQUIRED = 5;
|
|
15469
|
+
var EXIT_REMOTE_UNAVAILABLE = 6;
|
|
15470
|
+
var EXIT_INTEGRITY_FAILURE = 7;
|
|
15471
|
+
var EXIT_OFFLINE_CACHE_MISS = 8;
|
|
15472
|
+
var EXIT_INCOMPATIBLE_VERSION = 9;
|
|
15407
15473
|
var ConflictError = class extends Error {
|
|
15408
15474
|
constructor(message) {
|
|
15409
15475
|
super(message);
|
|
@@ -15416,6 +15482,44 @@ var TargetNotDetectedError = class extends Error {
|
|
|
15416
15482
|
this.name = "TargetNotDetectedError";
|
|
15417
15483
|
}
|
|
15418
15484
|
};
|
|
15485
|
+
var AuthenticationError = class extends Error {
|
|
15486
|
+
constructor(message) {
|
|
15487
|
+
super(message);
|
|
15488
|
+
this.name = "AuthenticationError";
|
|
15489
|
+
}
|
|
15490
|
+
};
|
|
15491
|
+
var RemoteUnavailableError = class extends Error {
|
|
15492
|
+
constructor(message) {
|
|
15493
|
+
super(message);
|
|
15494
|
+
this.name = "RemoteUnavailableError";
|
|
15495
|
+
}
|
|
15496
|
+
};
|
|
15497
|
+
var IntegrityError = class extends Error {
|
|
15498
|
+
constructor(message) {
|
|
15499
|
+
super(message);
|
|
15500
|
+
this.name = "IntegrityError";
|
|
15501
|
+
}
|
|
15502
|
+
};
|
|
15503
|
+
var OfflineCacheMissError = class extends Error {
|
|
15504
|
+
constructor(message) {
|
|
15505
|
+
super(message);
|
|
15506
|
+
this.name = "OfflineCacheMissError";
|
|
15507
|
+
}
|
|
15508
|
+
};
|
|
15509
|
+
var IncompatibleVersionError = class extends Error {
|
|
15510
|
+
constructor(message) {
|
|
15511
|
+
super(message);
|
|
15512
|
+
this.name = "IncompatibleVersionError";
|
|
15513
|
+
}
|
|
15514
|
+
};
|
|
15515
|
+
var CredentialsLegacyError = class extends Error {
|
|
15516
|
+
constructor() {
|
|
15517
|
+
super(
|
|
15518
|
+
"Legacy single-key credential file detected. The credential-store layout changed to store a client ID/secret pair (the credential-pair format). Run 'tangyr auth login' to re-authenticate and store the new format."
|
|
15519
|
+
);
|
|
15520
|
+
this.name = "CredentialsLegacyError";
|
|
15521
|
+
}
|
|
15522
|
+
};
|
|
15419
15523
|
function handleFatalError(err, logger) {
|
|
15420
15524
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
15421
15525
|
logger.error(error.message);
|
|
@@ -15428,6 +15532,24 @@ function handleFatalError(err, logger) {
|
|
|
15428
15532
|
if (error instanceof TargetNotDetectedError) {
|
|
15429
15533
|
process.exit(EXIT_TARGET_NOT_DETECTED);
|
|
15430
15534
|
}
|
|
15535
|
+
if (error instanceof AuthenticationError) {
|
|
15536
|
+
process.exit(EXIT_AUTH_REQUIRED);
|
|
15537
|
+
}
|
|
15538
|
+
if (error instanceof CredentialsLegacyError) {
|
|
15539
|
+
process.exit(EXIT_AUTH_REQUIRED);
|
|
15540
|
+
}
|
|
15541
|
+
if (error instanceof RemoteUnavailableError) {
|
|
15542
|
+
process.exit(EXIT_REMOTE_UNAVAILABLE);
|
|
15543
|
+
}
|
|
15544
|
+
if (error instanceof IntegrityError) {
|
|
15545
|
+
process.exit(EXIT_INTEGRITY_FAILURE);
|
|
15546
|
+
}
|
|
15547
|
+
if (error instanceof OfflineCacheMissError) {
|
|
15548
|
+
process.exit(EXIT_OFFLINE_CACHE_MISS);
|
|
15549
|
+
}
|
|
15550
|
+
if (error instanceof IncompatibleVersionError) {
|
|
15551
|
+
process.exit(EXIT_INCOMPATIBLE_VERSION);
|
|
15552
|
+
}
|
|
15431
15553
|
process.exit(EXIT_FAILURE);
|
|
15432
15554
|
}
|
|
15433
15555
|
function exitOnErrorSeverityLoss(report, logger, opts = {}) {
|
|
@@ -15444,6 +15566,28 @@ function exitOnErrorSeverityLoss(report, logger, opts = {}) {
|
|
|
15444
15566
|
}
|
|
15445
15567
|
}
|
|
15446
15568
|
|
|
15569
|
+
// src/core/runtime-guard.ts
|
|
15570
|
+
var MINIMUM_SUPPORTED_NODE_MAJOR = 22;
|
|
15571
|
+
function checkNodeRuntime(nodeVersion) {
|
|
15572
|
+
const match = /^v?(\d+)\./.exec(nodeVersion);
|
|
15573
|
+
const major = match ? Number.parseInt(match[1], 10) : Number.NaN;
|
|
15574
|
+
if (Number.isNaN(major) || major < MINIMUM_SUPPORTED_NODE_MAJOR) {
|
|
15575
|
+
return {
|
|
15576
|
+
supported: false,
|
|
15577
|
+
message: `tangyr requires Node.js >=${MINIMUM_SUPPORTED_NODE_MAJOR}. Detected ${nodeVersion}. Install Node.js ${MINIMUM_SUPPORTED_NODE_MAJOR} or later (e.g. via nvm: "nvm install ${MINIMUM_SUPPORTED_NODE_MAJOR}") and retry.`
|
|
15578
|
+
};
|
|
15579
|
+
}
|
|
15580
|
+
return { supported: true };
|
|
15581
|
+
}
|
|
15582
|
+
function assertSupportedNodeRuntime(nodeVersion = process.version) {
|
|
15583
|
+
const result = checkNodeRuntime(nodeVersion);
|
|
15584
|
+
if (!result.supported) {
|
|
15585
|
+
process.stderr.write(`${result.message}
|
|
15586
|
+
`);
|
|
15587
|
+
process.exit(EXIT_FAILURE);
|
|
15588
|
+
}
|
|
15589
|
+
}
|
|
15590
|
+
|
|
15447
15591
|
// src/commands/assess.ts
|
|
15448
15592
|
import fs11 from "fs";
|
|
15449
15593
|
import path12 from "path";
|
|
@@ -16926,63 +17070,364 @@ function resolveKitPath(config, configDir) {
|
|
|
16926
17070
|
import fs12 from "fs";
|
|
16927
17071
|
import os3 from "os";
|
|
16928
17072
|
import path13 from "path";
|
|
17073
|
+
|
|
17074
|
+
// src/core/transport.ts
|
|
17075
|
+
var KIT_CONTRACT_VERSION = "1";
|
|
17076
|
+
var CONTRACT_VERSION_HEADER = "X-Tangyr-Contract-Version";
|
|
17077
|
+
var ACCESS_CLIENT_ID_HEADER = "CF-Access-Client-Id";
|
|
17078
|
+
var ACCESS_CLIENT_SECRET_HEADER = "CF-Access-Client-Secret";
|
|
17079
|
+
var REDACTED = "[redacted]";
|
|
17080
|
+
function assertHttps(originUrl) {
|
|
17081
|
+
const parsed = new URL(originUrl);
|
|
17082
|
+
if (parsed.protocol !== "https:") {
|
|
17083
|
+
throw new IntegrityError(
|
|
17084
|
+
`KitTransport rejected a non-HTTPS origin "${originUrl}" \u2014 only https:// origins are permitted (REQ-INT-006).`
|
|
17085
|
+
);
|
|
17086
|
+
}
|
|
17087
|
+
}
|
|
17088
|
+
async function performRequest(originUrl, requestPath, credentials, logger) {
|
|
17089
|
+
assertHttps(originUrl);
|
|
17090
|
+
const headers = {
|
|
17091
|
+
[ACCESS_CLIENT_ID_HEADER]: credentials.clientId,
|
|
17092
|
+
[ACCESS_CLIENT_SECRET_HEADER]: credentials.clientSecret,
|
|
17093
|
+
[CONTRACT_VERSION_HEADER]: KIT_CONTRACT_VERSION
|
|
17094
|
+
};
|
|
17095
|
+
logger?.verbose(
|
|
17096
|
+
`KitTransport GET ${originUrl}${requestPath} \u2014 headers: ${ACCESS_CLIENT_ID_HEADER}=${REDACTED}, ${ACCESS_CLIENT_SECRET_HEADER}=${REDACTED}, ${CONTRACT_VERSION_HEADER}=${KIT_CONTRACT_VERSION}`
|
|
17097
|
+
);
|
|
17098
|
+
const response = await fetch(`${originUrl}${requestPath}`, {
|
|
17099
|
+
method: "GET",
|
|
17100
|
+
redirect: "manual",
|
|
17101
|
+
headers
|
|
17102
|
+
});
|
|
17103
|
+
if (response.status >= 300 && response.status < 400) {
|
|
17104
|
+
throw new IntegrityError(
|
|
17105
|
+
`KitTransport rejected a redirect response (HTTP ${response.status}) from ${originUrl}${requestPath} \u2014 redirects are never followed; every redirect is rejected fail-closed (REQ-INT-006, resolved reject-all-redirects policy).`
|
|
17106
|
+
);
|
|
17107
|
+
}
|
|
17108
|
+
if (response.status === 426) {
|
|
17109
|
+
throw new IncompatibleVersionError(
|
|
17110
|
+
`The origin rejected this CLI's contract version (HTTP 426) at ${originUrl}${requestPath}. Upgrade tangyr to the latest release and retry.`
|
|
17111
|
+
);
|
|
17112
|
+
}
|
|
17113
|
+
return response;
|
|
17114
|
+
}
|
|
17115
|
+
function createFetchKitTransport(options = {}) {
|
|
17116
|
+
return {
|
|
17117
|
+
async probe(originUrl, credentials) {
|
|
17118
|
+
const response = await performRequest(
|
|
17119
|
+
originUrl,
|
|
17120
|
+
"/v1/probe",
|
|
17121
|
+
credentials,
|
|
17122
|
+
options.logger
|
|
17123
|
+
);
|
|
17124
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
17125
|
+
const metadata = contentType.includes("application/json") ? await response.json() : {};
|
|
17126
|
+
return { status: response.status, metadata };
|
|
17127
|
+
},
|
|
17128
|
+
async fetchObject(originUrl, objectPath, credentials) {
|
|
17129
|
+
const response = await performRequest(
|
|
17130
|
+
originUrl,
|
|
17131
|
+
objectPath,
|
|
17132
|
+
credentials,
|
|
17133
|
+
options.logger
|
|
17134
|
+
);
|
|
17135
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
17136
|
+
return { status: response.status, body };
|
|
17137
|
+
}
|
|
17138
|
+
};
|
|
17139
|
+
}
|
|
17140
|
+
|
|
17141
|
+
// src/commands/auth.ts
|
|
16929
17142
|
var CREDENTIALS_DIR = path13.join(os3.homedir(), ".tangyr");
|
|
16930
17143
|
var CREDENTIALS_PATH = path13.join(CREDENTIALS_DIR, "credentials.json");
|
|
16931
|
-
|
|
16932
|
-
|
|
16933
|
-
|
|
16934
|
-
key = await dist_default6({ message: "Enter your Tangyr license key:" });
|
|
17144
|
+
function isCurrentCredentialRecord(value) {
|
|
17145
|
+
if (typeof value !== "object" || value === null) {
|
|
17146
|
+
return false;
|
|
16935
17147
|
}
|
|
16936
|
-
|
|
16937
|
-
|
|
16938
|
-
|
|
17148
|
+
const record = value;
|
|
17149
|
+
return record.schema === 2 && typeof record.clientId === "string" && record.clientId.length > 0 && typeof record.clientSecret === "string" && record.clientSecret.length > 0 && (record.validation === "validated" || record.validation === "unvalidated") && typeof record.storedAt === "string";
|
|
17150
|
+
}
|
|
17151
|
+
function readStoredCredentials() {
|
|
17152
|
+
if (!fs12.existsSync(CREDENTIALS_PATH)) {
|
|
17153
|
+
return null;
|
|
16939
17154
|
}
|
|
16940
|
-
fs12.
|
|
16941
|
-
const
|
|
16942
|
-
|
|
16943
|
-
|
|
16944
|
-
}
|
|
16945
|
-
|
|
16946
|
-
|
|
16947
|
-
|
|
16948
|
-
`,
|
|
16949
|
-
{ mode: 384 }
|
|
16950
|
-
);
|
|
17155
|
+
const raw = fs12.readFileSync(CREDENTIALS_PATH, "utf8");
|
|
17156
|
+
const parsed = JSON.parse(raw);
|
|
17157
|
+
if (!isCurrentCredentialRecord(parsed)) {
|
|
17158
|
+
throw new CredentialsLegacyError();
|
|
17159
|
+
}
|
|
17160
|
+
return parsed;
|
|
17161
|
+
}
|
|
17162
|
+
function ensureCredentialsDir(logger) {
|
|
16951
17163
|
try {
|
|
16952
|
-
fs12.
|
|
17164
|
+
fs12.mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 448 });
|
|
17165
|
+
} catch (err) {
|
|
17166
|
+
if (err.code !== "EEXIST") {
|
|
17167
|
+
throw err;
|
|
17168
|
+
}
|
|
17169
|
+
}
|
|
17170
|
+
if (fs12.lstatSync(CREDENTIALS_DIR).isSymbolicLink()) {
|
|
17171
|
+
throw new Error(
|
|
17172
|
+
`Refusing to write credentials: "${CREDENTIALS_DIR}" already exists as a symbolic link, not a directory. Remove it and run 'tangyr auth login' again.`
|
|
17173
|
+
);
|
|
17174
|
+
}
|
|
17175
|
+
try {
|
|
17176
|
+
fs12.chmodSync(CREDENTIALS_DIR, 448);
|
|
16953
17177
|
} catch {
|
|
17178
|
+
logger.warn(
|
|
17179
|
+
"Could not set owner-only (0700) permissions on the credentials directory on this platform; at-rest protection is reduced (REQ-AUT-007)."
|
|
17180
|
+
);
|
|
16954
17181
|
}
|
|
16955
|
-
|
|
16956
|
-
|
|
16957
|
-
logger
|
|
16958
|
-
|
|
17182
|
+
}
|
|
17183
|
+
function writeStoredCredentials(record, logger) {
|
|
17184
|
+
ensureCredentialsDir(logger);
|
|
17185
|
+
const content = `${JSON.stringify(record, null, 2)}
|
|
17186
|
+
`;
|
|
17187
|
+
const tmpPath = `${CREDENTIALS_PATH}.tmp`;
|
|
17188
|
+
try {
|
|
17189
|
+
fs12.writeFileSync(tmpPath, content, { mode: 384, flag: "wx" });
|
|
17190
|
+
} catch (err) {
|
|
17191
|
+
if (err.code === "EEXIST") {
|
|
17192
|
+
fs12.unlinkSync(tmpPath);
|
|
17193
|
+
fs12.writeFileSync(tmpPath, content, { mode: 384, flag: "wx" });
|
|
17194
|
+
} else {
|
|
17195
|
+
throw err;
|
|
17196
|
+
}
|
|
17197
|
+
}
|
|
17198
|
+
try {
|
|
17199
|
+
fs12.renameSync(tmpPath, CREDENTIALS_PATH);
|
|
17200
|
+
} catch (renameErr) {
|
|
17201
|
+
try {
|
|
17202
|
+
try {
|
|
17203
|
+
const destStat = fs12.lstatSync(CREDENTIALS_PATH);
|
|
17204
|
+
if (destStat.isSymbolicLink()) {
|
|
17205
|
+
fs12.unlinkSync(CREDENTIALS_PATH);
|
|
17206
|
+
}
|
|
17207
|
+
} catch {
|
|
17208
|
+
}
|
|
17209
|
+
fs12.copyFileSync(tmpPath, CREDENTIALS_PATH);
|
|
17210
|
+
try {
|
|
17211
|
+
fs12.chmodSync(CREDENTIALS_PATH, 384);
|
|
17212
|
+
} catch {
|
|
17213
|
+
logger.warn(
|
|
17214
|
+
"Could not set owner-only (0600) permissions on the credentials file on this platform; at-rest protection is reduced (REQ-AUT-007)."
|
|
17215
|
+
);
|
|
17216
|
+
}
|
|
17217
|
+
try {
|
|
17218
|
+
fs12.unlinkSync(tmpPath);
|
|
17219
|
+
} catch {
|
|
17220
|
+
}
|
|
17221
|
+
} catch (copyErr) {
|
|
17222
|
+
try {
|
|
17223
|
+
fs12.unlinkSync(tmpPath);
|
|
17224
|
+
} catch {
|
|
17225
|
+
}
|
|
17226
|
+
throw new Error(
|
|
17227
|
+
`writeStoredCredentials: failed to write ${CREDENTIALS_PATH} \u2014 ${String(renameErr)} / fallback copy failed \u2014 ${String(copyErr)}`
|
|
17228
|
+
);
|
|
17229
|
+
}
|
|
17230
|
+
}
|
|
17231
|
+
}
|
|
17232
|
+
var ADVANCE_EXPIRY_WARNING_DAYS = 30;
|
|
17233
|
+
var DEFINITIVE_DENIAL_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
17234
|
+
async function resolveCredentialForLogin(options, logger) {
|
|
17235
|
+
const envClientId = process.env.TANGYR_ACCESS_CLIENT_ID?.trim();
|
|
17236
|
+
const envClientSecret = process.env.TANGYR_ACCESS_CLIENT_SECRET?.trim();
|
|
17237
|
+
if (envClientId && envClientSecret) {
|
|
17238
|
+
logger.verbose(
|
|
17239
|
+
"Resolved credential from TANGYR_ACCESS_CLIENT_ID / TANGYR_ACCESS_CLIENT_SECRET."
|
|
17240
|
+
);
|
|
17241
|
+
return {
|
|
17242
|
+
pair: { clientId: envClientId, clientSecret: envClientSecret },
|
|
17243
|
+
source: "env"
|
|
17244
|
+
};
|
|
17245
|
+
}
|
|
17246
|
+
if (envClientId || envClientSecret) {
|
|
17247
|
+
throw new AuthenticationError(
|
|
17248
|
+
"Both TANGYR_ACCESS_CLIENT_ID and TANGYR_ACCESS_CLIENT_SECRET must be set together. Set both environment variables, or unset both and run 'tangyr auth login' interactively."
|
|
17249
|
+
);
|
|
17250
|
+
}
|
|
17251
|
+
const stored = readStoredCredentials();
|
|
17252
|
+
if (stored) {
|
|
17253
|
+
logger.verbose("Resolved credential from the stored credential file.");
|
|
17254
|
+
return {
|
|
17255
|
+
pair: { clientId: stored.clientId, clientSecret: stored.clientSecret },
|
|
17256
|
+
source: "stored"
|
|
17257
|
+
};
|
|
17258
|
+
}
|
|
17259
|
+
if (options.nonInteractive) {
|
|
17260
|
+
throw new AuthenticationError(
|
|
17261
|
+
"No credential available under --non-interactive. Set TANGYR_ACCESS_CLIENT_ID and TANGYR_ACCESS_CLIENT_SECRET, or drop --non-interactive and run 'tangyr auth login' again from an interactive terminal."
|
|
17262
|
+
);
|
|
17263
|
+
}
|
|
17264
|
+
const clientId = await dist_default6({
|
|
17265
|
+
message: "Enter your Tangyr access client ID:"
|
|
17266
|
+
});
|
|
17267
|
+
const clientSecret = await dist_default7({
|
|
17268
|
+
message: "Enter your Tangyr access client secret:"
|
|
17269
|
+
});
|
|
17270
|
+
if (clientId.trim().length === 0 || clientSecret.trim().length === 0) {
|
|
17271
|
+
throw new AuthenticationError("No credential provided.");
|
|
17272
|
+
}
|
|
17273
|
+
return {
|
|
17274
|
+
pair: { clientId: clientId.trim(), clientSecret: clientSecret.trim() },
|
|
17275
|
+
source: "interactive"
|
|
17276
|
+
};
|
|
17277
|
+
}
|
|
17278
|
+
function resolveOriginUrl(configPath) {
|
|
17279
|
+
let config;
|
|
17280
|
+
try {
|
|
17281
|
+
({ config } = resolveConfig(configPath));
|
|
17282
|
+
} catch (err) {
|
|
17283
|
+
if (err instanceof ConfigNotFoundError) {
|
|
17284
|
+
return void 0;
|
|
17285
|
+
}
|
|
17286
|
+
throw err;
|
|
17287
|
+
}
|
|
17288
|
+
const source = config.source;
|
|
17289
|
+
return typeof source.url === "string" && source.url.length > 0 ? source.url : void 0;
|
|
17290
|
+
}
|
|
17291
|
+
async function validateAgainstOrigin(originUrl, pair, transport, logger) {
|
|
17292
|
+
if (!originUrl) {
|
|
17293
|
+
logger.verbose(
|
|
17294
|
+
"No remote origin configured (source.url) \u2014 treating as unreachable for validation purposes."
|
|
17295
|
+
);
|
|
17296
|
+
return { outcome: "unreachable" };
|
|
17297
|
+
}
|
|
17298
|
+
try {
|
|
17299
|
+
const result = await transport.probe(originUrl, pair);
|
|
17300
|
+
if (DEFINITIVE_DENIAL_STATUSES.has(result.status)) {
|
|
17301
|
+
return { outcome: "denied" };
|
|
17302
|
+
}
|
|
17303
|
+
if (result.status >= 200 && result.status < 300) {
|
|
17304
|
+
return { outcome: "success", metadata: result.metadata };
|
|
17305
|
+
}
|
|
17306
|
+
return { outcome: "unreachable" };
|
|
17307
|
+
} catch (err) {
|
|
17308
|
+
if (err instanceof IntegrityError || err instanceof IncompatibleVersionError) {
|
|
17309
|
+
throw err;
|
|
17310
|
+
}
|
|
17311
|
+
logger.verbose(
|
|
17312
|
+
`Origin probe failed: ${err instanceof Error ? err.message : String(err)}`
|
|
17313
|
+
);
|
|
17314
|
+
return { outcome: "unreachable" };
|
|
17315
|
+
}
|
|
17316
|
+
}
|
|
17317
|
+
async function runAuthLoginCommand(options, logger, transport = createFetchKitTransport({ logger })) {
|
|
17318
|
+
const resolved = await resolveCredentialForLogin(options, logger);
|
|
17319
|
+
if (options.skipValidation) {
|
|
17320
|
+
writeStoredCredentials(
|
|
17321
|
+
{
|
|
17322
|
+
schema: 2,
|
|
17323
|
+
clientId: resolved.pair.clientId,
|
|
17324
|
+
clientSecret: resolved.pair.clientSecret,
|
|
17325
|
+
validation: "unvalidated",
|
|
17326
|
+
storedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17327
|
+
},
|
|
17328
|
+
logger
|
|
17329
|
+
);
|
|
17330
|
+
logger.info(
|
|
17331
|
+
"Credential stored (validation skipped via --skip-validation)."
|
|
17332
|
+
);
|
|
17333
|
+
logger.warn(
|
|
17334
|
+
"Validation state: stored, unvalidated. It will be revalidated on the next command that reaches the configured origin."
|
|
17335
|
+
);
|
|
17336
|
+
return;
|
|
17337
|
+
}
|
|
17338
|
+
const originUrl = resolveOriginUrl(options.config);
|
|
17339
|
+
const outcome = await validateAgainstOrigin(
|
|
17340
|
+
originUrl,
|
|
17341
|
+
resolved.pair,
|
|
17342
|
+
transport,
|
|
17343
|
+
logger
|
|
16959
17344
|
);
|
|
17345
|
+
if (outcome.outcome === "denied") {
|
|
17346
|
+
throw new AuthenticationError(
|
|
17347
|
+
"The configured origin denied this credential. The credential was not stored. Obtain a valid client ID/secret pair and run 'tangyr auth login' again."
|
|
17348
|
+
);
|
|
17349
|
+
}
|
|
17350
|
+
writeStoredCredentials(
|
|
17351
|
+
{
|
|
17352
|
+
schema: 2,
|
|
17353
|
+
clientId: resolved.pair.clientId,
|
|
17354
|
+
clientSecret: resolved.pair.clientSecret,
|
|
17355
|
+
validation: outcome.outcome === "success" ? "validated" : "unvalidated",
|
|
17356
|
+
storedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17357
|
+
},
|
|
17358
|
+
logger
|
|
17359
|
+
);
|
|
17360
|
+
if (outcome.outcome === "success") {
|
|
17361
|
+
logger.info("Credential validated and stored (stored, validated).");
|
|
17362
|
+
} else {
|
|
17363
|
+
logger.warn(
|
|
17364
|
+
"Could not reach the configured origin to validate this credential. Credential stored as stored, unvalidated \u2014 it will be revalidated on the next command that reaches the origin."
|
|
17365
|
+
);
|
|
17366
|
+
}
|
|
16960
17367
|
}
|
|
16961
|
-
function runAuthStatusCommand(
|
|
16962
|
-
|
|
17368
|
+
async function runAuthStatusCommand(options, logger, transport = createFetchKitTransport({ logger })) {
|
|
17369
|
+
const stored = readStoredCredentials();
|
|
17370
|
+
if (!stored) {
|
|
16963
17371
|
logger.info(
|
|
16964
|
-
"Not authenticated. Run 'tangyr auth login' to configure a
|
|
17372
|
+
"Not authenticated. Run 'tangyr auth login' to configure a credential."
|
|
16965
17373
|
);
|
|
16966
17374
|
return;
|
|
16967
17375
|
}
|
|
16968
|
-
|
|
16969
|
-
|
|
16970
|
-
|
|
16971
|
-
|
|
16972
|
-
|
|
16973
|
-
logger
|
|
16974
|
-
|
|
16975
|
-
|
|
16976
|
-
|
|
17376
|
+
const originUrl = resolveOriginUrl(options.config);
|
|
17377
|
+
const outcome = await validateAgainstOrigin(
|
|
17378
|
+
originUrl,
|
|
17379
|
+
{ clientId: stored.clientId, clientSecret: stored.clientSecret },
|
|
17380
|
+
transport,
|
|
17381
|
+
logger
|
|
17382
|
+
);
|
|
17383
|
+
if (outcome.outcome === "denied") {
|
|
17384
|
+
throw new AuthenticationError(
|
|
17385
|
+
"The configured origin denied the stored credential \u2014 it has likely been revoked. Run 'tangyr auth login' with a new credential."
|
|
17386
|
+
);
|
|
17387
|
+
}
|
|
17388
|
+
if (outcome.outcome === "unreachable") {
|
|
17389
|
+
logger.info(`Credential status: stored, ${stored.validation}.`);
|
|
17390
|
+
logger.info(
|
|
17391
|
+
"The configured origin was not reached \u2014 label and expiry are not reported."
|
|
17392
|
+
);
|
|
17393
|
+
return;
|
|
17394
|
+
}
|
|
17395
|
+
if (stored.validation === "unvalidated") {
|
|
17396
|
+
writeStoredCredentials(
|
|
17397
|
+
{
|
|
17398
|
+
...stored,
|
|
17399
|
+
validation: "validated",
|
|
17400
|
+
storedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17401
|
+
},
|
|
17402
|
+
logger
|
|
17403
|
+
);
|
|
17404
|
+
}
|
|
17405
|
+
logger.info("Credential status: stored, validated.");
|
|
17406
|
+
const label = outcome.metadata.label;
|
|
17407
|
+
if (typeof label === "string" && label.length > 0) {
|
|
17408
|
+
logger.info(`Label: ${label}`);
|
|
17409
|
+
}
|
|
17410
|
+
const expiresAtRaw = outcome.metadata.expiresAt;
|
|
17411
|
+
if (typeof expiresAtRaw === "string") {
|
|
17412
|
+
const expiresAt = new Date(expiresAtRaw);
|
|
17413
|
+
if (!Number.isNaN(expiresAt.getTime())) {
|
|
17414
|
+
logger.info(`Expires: ${expiresAt.toISOString()}`);
|
|
17415
|
+
const daysUntilExpiry = (expiresAt.getTime() - Date.now()) / (24 * 60 * 60 * 1e3);
|
|
17416
|
+
if (daysUntilExpiry <= ADVANCE_EXPIRY_WARNING_DAYS) {
|
|
17417
|
+
logger.warn(
|
|
17418
|
+
`Credential expires in ${Math.max(0, Math.ceil(daysUntilExpiry))} day(s) (${expiresAt.toISOString()}) \u2014 renew it soon.`
|
|
17419
|
+
);
|
|
17420
|
+
}
|
|
17421
|
+
}
|
|
16977
17422
|
}
|
|
16978
17423
|
}
|
|
16979
17424
|
function runAuthLogoutCommand(_options, logger) {
|
|
16980
17425
|
if (!fs12.existsSync(CREDENTIALS_PATH)) {
|
|
16981
|
-
logger.info("No
|
|
17426
|
+
logger.info("No credential found. Nothing to remove.");
|
|
16982
17427
|
return;
|
|
16983
17428
|
}
|
|
16984
17429
|
fs12.unlinkSync(CREDENTIALS_PATH);
|
|
16985
|
-
logger.info("
|
|
17430
|
+
logger.info("Credential removed.");
|
|
16986
17431
|
}
|
|
16987
17432
|
|
|
16988
17433
|
// src/commands/cleanup.ts
|
|
@@ -21698,14 +22143,14 @@ ${JSON.stringify(config, null, 2)}`);
|
|
|
21698
22143
|
checked: enabled
|
|
21699
22144
|
}))
|
|
21700
22145
|
});
|
|
21701
|
-
const selectedConflict = await
|
|
22146
|
+
const selectedConflict = await dist_default8({
|
|
21702
22147
|
message: "Conflict policy",
|
|
21703
22148
|
choices: ["ask", "skip", "backup", "overwrite"].map((value) => ({
|
|
21704
22149
|
name: value,
|
|
21705
22150
|
value
|
|
21706
22151
|
}))
|
|
21707
22152
|
});
|
|
21708
|
-
const selectedVerbosity = await
|
|
22153
|
+
const selectedVerbosity = await dist_default8({
|
|
21709
22154
|
message: "Verbosity",
|
|
21710
22155
|
choices: ["quiet", "normal", "verbose"].map((value) => ({
|
|
21711
22156
|
name: value,
|
|
@@ -21777,9 +22222,17 @@ var bundledCopilotManifestPathsByPlatform = {
|
|
|
21777
22222
|
};
|
|
21778
22223
|
function detectTargets(logger) {
|
|
21779
22224
|
const result = {
|
|
21780
|
-
"claude-code": commandExists(
|
|
22225
|
+
"claude-code": commandExists(
|
|
22226
|
+
process.platform === "win32" ? "where.exe" : "which",
|
|
22227
|
+
["claude"],
|
|
22228
|
+
logger
|
|
22229
|
+
),
|
|
21781
22230
|
copilot: detectsCopilot(logger),
|
|
21782
|
-
codex: commandExists(
|
|
22231
|
+
codex: commandExists(
|
|
22232
|
+
process.platform === "win32" ? "where.exe" : "which",
|
|
22233
|
+
["codex"],
|
|
22234
|
+
logger
|
|
22235
|
+
)
|
|
21783
22236
|
};
|
|
21784
22237
|
return result;
|
|
21785
22238
|
}
|
|
@@ -21809,9 +22262,16 @@ function detectCopilot(logger, dependencies = {}) {
|
|
|
21809
22262
|
...bundledCopilotEditorCommandsByPlatform[platform] ?? []
|
|
21810
22263
|
];
|
|
21811
22264
|
for (const command of commandCandidates) {
|
|
21812
|
-
const result = spawnSyncImpl(command, [
|
|
22265
|
+
const result = spawnSyncImpl(command, [
|
|
22266
|
+
"--list-extensions",
|
|
22267
|
+
"--show-versions"
|
|
22268
|
+
]);
|
|
21813
22269
|
if (result.error) {
|
|
21814
|
-
logDetectionError(
|
|
22270
|
+
logDetectionError(
|
|
22271
|
+
`${command} --list-extensions --show-versions`,
|
|
22272
|
+
result.error,
|
|
22273
|
+
logger
|
|
22274
|
+
);
|
|
21815
22275
|
continue;
|
|
21816
22276
|
}
|
|
21817
22277
|
if (result.status !== 0) {
|
|
@@ -21892,10 +22352,14 @@ function commandExists(command, args, logger) {
|
|
|
21892
22352
|
}
|
|
21893
22353
|
function logDetectionError(attemptedCommand, error, logger) {
|
|
21894
22354
|
if (error.code === "ENOENT") {
|
|
21895
|
-
logger?.verbose(
|
|
22355
|
+
logger?.verbose(
|
|
22356
|
+
`${attemptedCommand}: executable not found while detecting target`
|
|
22357
|
+
);
|
|
21896
22358
|
return;
|
|
21897
22359
|
}
|
|
21898
|
-
logger?.verbose(
|
|
22360
|
+
logger?.verbose(
|
|
22361
|
+
`${attemptedCommand} failed: ${error.message ?? "unknown spawn error"}`
|
|
22362
|
+
);
|
|
21899
22363
|
}
|
|
21900
22364
|
|
|
21901
22365
|
// src/commands/doctor.ts
|
|
@@ -22210,7 +22674,7 @@ async function runInitCommand(_options, logger) {
|
|
|
22210
22674
|
checked: detected
|
|
22211
22675
|
}))
|
|
22212
22676
|
});
|
|
22213
|
-
const onConflict = await
|
|
22677
|
+
const onConflict = await dist_default8({
|
|
22214
22678
|
message: "Conflict policy",
|
|
22215
22679
|
choices: ["ask", "skip", "backup", "overwrite"].map((value) => ({
|
|
22216
22680
|
name: value,
|
|
@@ -26207,7 +26671,7 @@ async function runInstallCommand(options, logger) {
|
|
|
26207
26671
|
);
|
|
26208
26672
|
process.exit(1);
|
|
26209
26673
|
} else {
|
|
26210
|
-
action = await
|
|
26674
|
+
action = await dist_default8({
|
|
26211
26675
|
message: "Existing installation found. Choose an action:",
|
|
26212
26676
|
choices: [
|
|
26213
26677
|
{
|
|
@@ -26336,7 +26800,7 @@ Found ${conflictingFindings.length} conflicting target(s). Please choose an acti
|
|
|
26336
26800
|
`
|
|
26337
26801
|
);
|
|
26338
26802
|
for (const finding of conflictingFindings) {
|
|
26339
|
-
const choice = await
|
|
26803
|
+
const choice = await dist_default8({
|
|
26340
26804
|
message: `Conflict: ${finding.path} (${finding.tool})
|
|
26341
26805
|
Choose an action:`,
|
|
26342
26806
|
choices: [
|
|
@@ -30081,6 +30545,7 @@ var commandNames = [
|
|
|
30081
30545
|
"probe"
|
|
30082
30546
|
];
|
|
30083
30547
|
async function main() {
|
|
30548
|
+
assertSupportedNodeRuntime();
|
|
30084
30549
|
process.on("SIGINT", () => {
|
|
30085
30550
|
process.exit(EXIT_USER_INTERRUPT);
|
|
30086
30551
|
});
|
|
@@ -30111,6 +30576,9 @@ function buildProgram() {
|
|
|
30111
30576
|
).option(
|
|
30112
30577
|
"-y, --yes",
|
|
30113
30578
|
"Skip interactive confirmation and apply non-interactive conflict handling"
|
|
30579
|
+
).option(
|
|
30580
|
+
"--non-interactive",
|
|
30581
|
+
"Never prompt; fail closed when a required input is missing (auth login)"
|
|
30114
30582
|
).option("-v, --verbose", "Emit detailed output").option("-q, --quiet", "Emit errors only").option("--no-color", "Disable colorized output").option(
|
|
30115
30583
|
"--claude-config-dir <path>",
|
|
30116
30584
|
"Claude Code global root directory (repeatable; overrides claudeCodeGlobalPaths in config)",
|
|
@@ -30137,19 +30605,23 @@ function buildProgram() {
|
|
|
30137
30605
|
);
|
|
30138
30606
|
}
|
|
30139
30607
|
);
|
|
30140
|
-
const authCmd = program2.command("auth").description("
|
|
30141
|
-
authCmd.command("login").description("Authenticate with a
|
|
30608
|
+
const authCmd = program2.command("auth").description("Credential management");
|
|
30609
|
+
authCmd.command("login").description("Authenticate with a client ID/secret credential pair").option(
|
|
30610
|
+
"--skip-validation",
|
|
30611
|
+
"Store the credential without contacting the configured origin"
|
|
30612
|
+
).action(async (options) => {
|
|
30142
30613
|
const context = commandContext(program2);
|
|
30614
|
+
const nonInteractive = context.options.nonInteractive === true || !process.stdin.isTTY;
|
|
30143
30615
|
await runAuthLoginCommand(
|
|
30144
|
-
{ ...context.options, ...options },
|
|
30616
|
+
{ ...context.options, ...options, nonInteractive },
|
|
30145
30617
|
context.logger
|
|
30146
30618
|
);
|
|
30147
30619
|
});
|
|
30148
|
-
authCmd.command("status").description("Show current authentication status").action(() => {
|
|
30620
|
+
authCmd.command("status").description("Show current authentication status").action(async () => {
|
|
30149
30621
|
const context = commandContext(program2);
|
|
30150
|
-
runAuthStatusCommand({ ...context.options }, context.logger);
|
|
30622
|
+
await runAuthStatusCommand({ ...context.options }, context.logger);
|
|
30151
30623
|
});
|
|
30152
|
-
authCmd.command("logout").description("Remove stored
|
|
30624
|
+
authCmd.command("logout").description("Remove stored credential").action(() => {
|
|
30153
30625
|
const context = commandContext(program2);
|
|
30154
30626
|
runAuthLogoutCommand({ ...context.options }, context.logger);
|
|
30155
30627
|
});
|
|
@@ -30238,7 +30710,7 @@ function buildProgram() {
|
|
|
30238
30710
|
return program2;
|
|
30239
30711
|
}
|
|
30240
30712
|
async function runInteractiveMenu(program2, logger) {
|
|
30241
|
-
const choice = await
|
|
30713
|
+
const choice = await dist_default8({
|
|
30242
30714
|
message: "Select a Tangyr command",
|
|
30243
30715
|
choices: [
|
|
30244
30716
|
{ name: "Assess", value: "assess" },
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alessandroraffa/tangyr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "CLI for the Tangyr discipline — install and manage operating kits for AI coding tools",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
6
9
|
"type": "module",
|
|
7
10
|
"bin": {
|
|
8
11
|
"tangyr": "./dist/index.js"
|
|
@@ -33,9 +36,13 @@
|
|
|
33
36
|
"bugs": {
|
|
34
37
|
"url": "https://github.com/alessandroraffa/tangyr-cli/issues"
|
|
35
38
|
},
|
|
39
|
+
"overrides": {
|
|
40
|
+
"esbuild": "^0.28.1"
|
|
41
|
+
},
|
|
36
42
|
"devDependencies": {
|
|
37
43
|
"@iarna/toml": "^2.2.5",
|
|
38
44
|
"@inquirer/prompts": "^8.4.1",
|
|
45
|
+
"@types/node": "^22.20.1",
|
|
39
46
|
"chalk": "^5.6.2",
|
|
40
47
|
"commander": "^14.0.3",
|
|
41
48
|
"execa": "^9.0.0",
|
|
@@ -46,6 +53,5 @@
|
|
|
46
53
|
"typescript": "^5.0.0",
|
|
47
54
|
"vitest": "^3.0.0",
|
|
48
55
|
"yaml": "^2.8.3"
|
|
49
|
-
}
|
|
50
|
-
"dependencies": {}
|
|
56
|
+
}
|
|
51
57
|
}
|