Skip to content

Commit 3675a1a

Browse files
committed
feat(scope): add global and local profile scopes
Closes tago-io/project-sdk-and-tools#4. Adds a parent-walk resolver that finds tagoconfig.json from any subdirectory and falls back to a per-user global config under XDG/AppData. New `tagoio whoami` command and `--scope local|global` flag on init/login, with mutating-command stderr banner, secure 0o700/0o600 perms on global, and analysis-* commands gated to local scope.
1 parent 19785ef commit 3675a1a

42 files changed

Lines changed: 1433 additions & 131 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/commands/analysis/analysis-console.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Account, AnalysisInfo } from "@tago-io/sdk";
22
import { EventSource } from "eventsource";
33
import { getEnvironmentConfig, IEnvironment } from "../../lib/config-file.js";
44
import { errorHandler, highlightMSG, infoMSG, successMSG } from "../../lib/messages.js";
5+
import { requireLocalScope } from "../../lib/resolve-scope.js";
56
import { searchName } from "../../lib/search-name.js";
67
import { pickAnalysisFromConfig } from "../../prompt/pick-analysis-from-config.js";
78

@@ -25,8 +26,8 @@ function apiSSE(profileToken: string, analysisID: string, urlSSERealtime?: strin
2526
* @param analysisList - The list of analysis objects to search through.
2627
* @returns The script object that matches the script name or the one selected by the user.
2728
*/
28-
async function getScriptObj(scriptName: string | void, analysisList: IEnvironment["analysisList"]) {
29-
let scriptObj: IEnvironment["analysisList"][0] | undefined;
29+
async function getScriptObj(scriptName: string | void, analysisList: NonNullable<IEnvironment["analysisList"]>) {
30+
let scriptObj: NonNullable<IEnvironment["analysisList"]>[number] | undefined;
3031
if (scriptName) {
3132
scriptObj = searchName(
3233
scriptName,
@@ -69,12 +70,14 @@ function setupSSE(sse: ReturnType<typeof apiSSE>, _script_id: string, analysis_i
6970
* @returns void
7071
*/
7172
async function connectAnalysisConsole(scriptName: string | void, options: { environment: string }) {
73+
requireLocalScope("analysis-console");
74+
7275
const config = getEnvironmentConfig(options.environment);
7376
if (!config || !config.profileToken) {
7477
errorHandler("Environment not found");
7578
}
7679

77-
const scriptObj = await getScriptObj(scriptName, config.analysisList);
80+
const scriptObj = await getScriptObj(scriptName, config.analysisList ?? []);
7881
if (!scriptObj) {
7982
errorHandler(`Analysis not found: ${scriptName}`);
8083
}

src/commands/analysis/analysis-set-mode.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import kleur from "kleur";
33

44
import { getEnvironmentConfig } from "../../lib/config-file.js";
55
import { errorHandler, infoMSG, successMSG } from "../../lib/messages.js";
6+
import { requireLocalScope } from "../../lib/resolve-scope.js";
67
import { chooseFromList } from "../../prompt/choose-from-list.js";
78
import { pickFromList } from "../../prompt/pick-from-list.js";
89

@@ -62,6 +63,8 @@ async function chooseAnalysisToUpdateRunOnMode(
6263
* @param options.filterMode - The filter mode to use when retrieving the analysis list.
6364
*/
6465
async function analysisSetMode(userInputName: string | void, options: { environment: string; mode: string; filterMode: string }) {
66+
requireLocalScope("analysis-mode");
67+
6568
const config = getEnvironmentConfig(options.environment);
6669
if (!config || !config.profileToken) {
6770
errorHandler("Environment not found");

src/commands/analysis/deploy.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,18 @@ vi.mock("../../lib/current-runtime.js", () => ({
4646
detectRuntime: detectRuntimeMock,
4747
}));
4848

49-
vi.mock("../../lib/get-current-folder.js", () => ({
50-
getCurrentFolder: () => "/repo",
49+
vi.mock("../../lib/resolve-scope.js", () => ({
50+
requireLocalScope: () => ({
51+
scope: "local" as const,
52+
root: "/repo",
53+
configPath: "/repo/tagoconfig.json",
54+
envFilePath: "/repo/.tagoio/personal.env",
55+
configExists: true,
56+
}),
57+
}));
58+
59+
vi.mock("../../lib/scope-notice.js", () => ({
60+
printScopeBanner: vi.fn(),
5161
}));
5262

5363
vi.mock("../../lib/messages.js", () => ({

src/commands/analysis/deploy.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { Account, RunTypeOptions } from "@tago-io/sdk";
55

66
import { getEnvironmentConfig, IConfigFile, IEnvironment } from "../../lib/config-file.js";
77
import { detectRuntime } from "../../lib/current-runtime.js";
8-
import { getCurrentFolder } from "../../lib/get-current-folder.js";
98
import { errorHandler, infoMSG, successMSG } from "../../lib/messages.js";
9+
import { requireLocalScope } from "../../lib/resolve-scope.js";
10+
import { printScopeBanner } from "../../lib/scope-notice.js";
1011
import { searchName } from "../../lib/search-name.js";
1112
import { chooseAnalysisListFromConfig } from "../../prompt/choose-analysis-list-config.js";
1213
import { confirmAnalysisFromConfig } from "../../prompt/confirm-analysis-list.js";
@@ -20,18 +21,19 @@ interface BuildScriptParams {
2021
config: EnvConfig;
2122
runtime: string;
2223
path: string;
24+
/** Resolved local-scope project root. */
25+
projectRoot: string;
2326
}
2427

2528
/**
2629
* Returns an object containing the paths for analysis, build and current folder.
2730
* @param config - An object containing the configuration for the environment.
2831
* @returns An object containing the paths for analysis, build and current folder.
2932
*/
30-
function getPaths(config: EnvConfig) {
31-
const folderPath = getCurrentFolder();
33+
function getPaths(config: EnvConfig, projectRoot: string) {
3234
const buildPath = config.buildPath || `./build`;
3335
const analysisPath = config.analysisPath || `./src/analysis`;
34-
return { analysisPath, buildPath, folderPath };
36+
return { analysisPath, buildPath, folderPath: projectRoot };
3537
}
3638

3739
/**
@@ -64,8 +66,8 @@ async function deleteOldFile(buildedFile: string) {
6466
* @param params - The parameters for building and uploading the script.
6567
*/
6668
async function buildScript(params: BuildScriptParams) {
67-
const { account, scriptName, analysisID, config, runtime, path } = params;
68-
const { analysisPath, buildPath, folderPath } = getPaths(config);
69+
const { account, scriptName, analysisID, config, runtime, path, projectRoot } = params;
70+
const { analysisPath, buildPath, folderPath } = getPaths(config, projectRoot);
6971

7072
let analysisFile;
7173
if (path) {
@@ -142,6 +144,10 @@ async function deployAnalysis(cmdScriptName: string, options: IDeployOptions) {
142144
errorHandler('Did you mean "tagoio deploy --all"? The "all" positional argument is no longer supported.');
143145
}
144146

147+
// Analysis development requires a project directory.
148+
const scope = requireLocalScope("analysis-deploy");
149+
printScopeBanner(scope, options.silent);
150+
145151
const config = getEnvironmentConfig(options.environment);
146152
if (!config) {
147153
errorHandler("Environment not found");
@@ -155,12 +161,12 @@ async function deployAnalysis(cmdScriptName: string, options: IDeployOptions) {
155161
}
156162

157163
// --all skips selection entirely; everything in analysisList with a fileName ships.
158-
let scriptList = config.analysisList.filter((x) => x.fileName);
164+
let scriptList = (config.analysisList ?? []).filter((x) => x.fileName);
159165
if (!options.all) {
160166
if (!cmdScriptName) {
161167
scriptList = await chooseAnalysisListFromConfig(scriptList);
162168
} else {
163-
const analysisFound: IEnvironment["analysisList"][0] = searchName(
169+
const analysisFound: NonNullable<IEnvironment["analysisList"]>[number] = searchName(
164170
cmdScriptName,
165171
scriptList.map((x) => ({ names: [x.name, x.fileName], value: x })),
166172
);
@@ -202,6 +208,7 @@ async function deployAnalysis(cmdScriptName: string, options: IDeployOptions) {
202208
config,
203209
runtime,
204210
path: path || "",
211+
projectRoot: scope.root,
205212
});
206213
}
207214
process.exit();

src/commands/analysis/duplicate-analysis.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import zlib from "node:zlib";
55

66
import { getEnvironmentConfig } from "../../lib/config-file.js";
77
import { errorHandler, successMSG } from "../../lib/messages.js";
8+
import { requireLocalScope } from "../../lib/resolve-scope.js";
89
import { pickAnalysisFromTagoIO } from "../../prompt/pick-analysis-from-tagoio.js";
910

1011
/**
@@ -78,6 +79,8 @@ async function downloadScriptBase64(account: Account, analysisId: string): Promi
7879
* @throws An error if the analysis ID is not found or if the environment is not found.
7980
*/
8081
async function duplicateAnalysis(analysisID: string | void, options: { environment: string; name?: string }) {
82+
requireLocalScope("analysis-duplicate");
83+
8184
const config = getEnvironmentConfig(options.environment);
8285
if (!config || !config.profileToken) {
8386
errorHandler("Environment not found");

src/commands/analysis/run-analysis.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,14 @@ vi.mock("../../lib/current-runtime.js", () => ({
3636
detectRuntime: detectRuntimeMock,
3737
}));
3838

39-
vi.mock("../../lib/get-current-folder.js", () => ({
40-
getCurrentFolder: () => "/tmp/test",
39+
vi.mock("../../lib/resolve-scope.js", () => ({
40+
requireLocalScope: () => ({
41+
scope: "local" as const,
42+
root: "/tmp/test",
43+
configPath: "/tmp/test/tagoconfig.json",
44+
envFilePath: "/tmp/test/.tagoio/personal.env",
45+
configExists: true,
46+
}),
4147
}));
4248

4349
vi.mock("../../lib/messages.js", () => ({

src/commands/analysis/run-analysis.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { Account } from "@tago-io/sdk";
55

66
import { getEnvironmentConfig, IEnvironment, resolveCLIPath } from "../../lib/config-file.js";
77
import { detectRuntime } from "../../lib/current-runtime.js";
8-
import { getCurrentFolder } from "../../lib/get-current-folder.js";
98
import { errorHandler, highlightMSG, successMSG } from "../../lib/messages.js";
9+
import { requireLocalScope } from "../../lib/resolve-scope.js";
1010
import { searchName } from "../../lib/search-name.js";
1111
import { pickAnalysisFromConfig } from "../../prompt/pick-analysis-from-config.js";
1212

@@ -68,13 +68,16 @@ async function runAnalysis(
6868
scriptName: string | undefined,
6969
options: { environment: string; debug: boolean; clear: boolean; tsnd: boolean; deno: boolean; node: boolean },
7070
) {
71+
// Analysis development requires a project directory.
72+
const scope = requireLocalScope("analysis-run");
73+
7174
const config = getEnvironmentConfig(options.environment);
7275
if (!config || !config.profileToken) {
7376
errorHandler("Environment not found");
7477
}
7578

76-
const analysisList = config.analysisList.filter((x) => x.fileName);
77-
let scriptToRun: IEnvironment["analysisList"][0];
79+
const analysisList = (config.analysisList ?? []).filter((x) => x.fileName);
80+
let scriptToRun: NonNullable<IEnvironment["analysisList"]>[number];
7881
if (scriptName) {
7982
scriptName = scriptName.toLowerCase();
8083
scriptToRun = searchName(
@@ -111,7 +114,7 @@ async function runAnalysis(
111114

112115
const spawnOptions: SpawnOptions = {
113116
shell: true,
114-
cwd: getCurrentFolder(),
117+
cwd: scope.root,
115118
stdio: "inherit",
116119
env: analysisEnv,
117120
};

src/commands/analysis/trigger-analysis.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import kleur from "kleur";
33

44
import { getEnvironmentConfig, IEnvironment } from "../../lib/config-file.js";
55
import { errorHandler, infoMSG, successMSG } from "../../lib/messages.js";
6+
import { requireLocalScope } from "../../lib/resolve-scope.js";
67
import { searchName } from "../../lib/search-name.js";
78
import { pickAnalysisFromConfig } from "../../prompt/pick-analysis-from-config.js";
89
import { pickAnalysisFromTagoIO } from "../../prompt/pick-analysis-from-tagoio.js";
@@ -16,16 +17,19 @@ import { pickAnalysisFromTagoIO } from "../../prompt/pick-analysis-from-tagoio.j
1617
* @param options.tago - Whether to pick the analysis from TagoIO.
1718
*/
1819
async function triggerAnalysis(scriptName: string | void, options: { environment?: string; json?: string; tago: boolean }) {
20+
requireLocalScope("analysis-trigger");
21+
1922
const config = getEnvironmentConfig(options.environment);
2023

2124
if (!config || !config.profileToken) {
2225
errorHandler("Environment not found");
2326
}
2427

2528
const account = new Account({ token: config.profileToken, region: config.profileRegion });
26-
const analysisList = config.analysisList.filter((x) => x.fileName);
29+
const fullList = config.analysisList ?? [];
30+
const analysisList = fullList.filter((x) => x.fileName);
2731

28-
let script: IEnvironment["analysisList"][0] | undefined;
32+
let script: NonNullable<IEnvironment["analysisList"]>[number] | undefined;
2933

3034
if (!scriptName && options.tago) {
3135
const analysis = await pickAnalysisFromTagoIO(account);
@@ -35,7 +39,7 @@ async function triggerAnalysis(scriptName: string | void, options: { environment
3539
} else {
3640
script = searchName(
3741
scriptName,
38-
config.analysisList.map((x) => ({ names: [x.name, x.fileName], value: x })),
42+
fullList.map((x) => ({ names: [x.name, x.fileName], value: x })),
3943
);
4044
}
4145

src/commands/devices/change-bucket-type.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import kleur from "kleur";
33

44
import { getEnvironmentConfig } from "../../lib/config-file.js";
55
import { errorHandler, infoMSG, successMSG } from "../../lib/messages.js";
6+
import { resolveScope } from "../../lib/resolve-scope.js";
7+
import { printScopeBanner } from "../../lib/scope-notice.js";
68
import { chooseFromList } from "../../prompt/choose-from-list.js";
79
import { promptNumber } from "../../prompt/number-prompt.js";
810
import { pickFromList } from "../../prompt/pick-from-list.js";
@@ -97,6 +99,8 @@ async function chooseBucketsFromList(account: Account) {
9799
}
98100

99101
async function changeBucketType(id: string, options: { environment: string }) {
102+
printScopeBanner(resolveScope());
103+
100104
const config = getEnvironmentConfig(options.environment);
101105
if (!config || !config.profileToken) {
102106
errorHandler("Environment not found");

src/commands/devices/change-network.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ vi.mock("../../lib/messages.js", () => ({
2929
successMSG: vi.fn(),
3030
}));
3131

32+
vi.mock("../../lib/resolve-scope.js", () => ({
33+
resolveScope: () => ({
34+
scope: "local" as const,
35+
root: "/repo",
36+
configPath: "/repo/tagoconfig.json",
37+
envFilePath: "/repo/.tagoio/personal.env",
38+
configExists: true,
39+
}),
40+
}));
41+
42+
vi.mock("../../lib/scope-notice.js", () => ({
43+
printScopeBanner: vi.fn(),
44+
}));
45+
3246
vi.mock("../../prompt/pick-device-id-from-tagoio.js", () => ({
3347
pickDeviceIDFromTagoIO: (...args: unknown[]) => pickDeviceIDFromTagoIOMock(...args),
3448
}));

0 commit comments

Comments
 (0)