@martini-film/cli 0.1.0 → 0.3.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 +49 -28
- package/dist/api.js +38 -14
- package/dist/api.js.map +1 -1
- package/dist/approve.js +83 -0
- package/dist/approve.js.map +1 -0
- package/dist/args.js +33 -12
- package/dist/args.js.map +1 -1
- package/dist/cli.js +202 -64
- package/dist/cli.js.map +1 -1
- package/dist/commands/assets.js +255 -46
- package/dist/commands/assets.js.map +1 -1
- package/dist/commands/context.js +42 -8
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/files.js +135 -95
- package/dist/commands/files.js.map +1 -1
- package/dist/commands/login.js +171 -30
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/tools.js +73 -12
- package/dist/commands/tools.js.map +1 -1
- package/dist/config.js +10 -19
- package/dist/config.js.map +1 -1
- package/dist/generated/manifest.json +18393 -5431
- package/dist/help.js +203 -18
- package/dist/help.js.map +1 -1
- package/dist/host.js +0 -6
- package/dist/host.js.map +1 -1
- package/dist/manifest.js +9 -10
- package/dist/manifest.js.map +1 -1
- package/dist/output.js +149 -20
- package/dist/output.js.map +1 -1
- package/dist/schema.js +103 -30
- package/dist/schema.js.map +1 -1
- package/dist/wait.js +211 -29
- package/dist/wait.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/files.js
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `martini files`: the project's memory files as a directory.
|
|
3
|
-
*
|
|
4
|
-
* ls list_project_files
|
|
5
|
-
* cat <path> read_project_file (content to stdout)
|
|
6
|
-
* write <path> [@file|-] write_project_file (replace; --append; --create-only)
|
|
7
|
-
* pull <dir> every file to disk plus .martini/files.json (path → updatedAt)
|
|
8
|
-
* push <dir> changed files back, each guarded by expectedUpdatedAt from the
|
|
9
|
-
* manifest: a file edited server-side since the pull is refused
|
|
10
|
-
* (PROJECT_FILE_CHANGED) unless --force. Never deletes.
|
|
11
|
-
*
|
|
12
|
-
* project.md is human-owned and Instructions files are read-only during a run;
|
|
13
|
-
* the server refuses them and push reports them as skipped.
|
|
14
|
-
*/
|
|
15
1
|
import { createHash } from 'node:crypto';
|
|
16
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
17
3
|
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
@@ -33,8 +19,25 @@ function readContentArgument(raw) {
|
|
|
33
19
|
return raw;
|
|
34
20
|
}
|
|
35
21
|
async function listFiles(context, projectId) {
|
|
36
|
-
const
|
|
37
|
-
|
|
22
|
+
const files = [];
|
|
23
|
+
let after;
|
|
24
|
+
for (;;) {
|
|
25
|
+
const page = await context.client.callTool('list_project_files', {
|
|
26
|
+
projectId,
|
|
27
|
+
...(after === undefined ? {} : { after }),
|
|
28
|
+
});
|
|
29
|
+
files.push(...(page.files ?? []));
|
|
30
|
+
if (typeof page.nextAfter !== 'string' || page.nextAfter === '')
|
|
31
|
+
return files;
|
|
32
|
+
if (page.nextAfter === after) {
|
|
33
|
+
throw new CliApiError(`list_project_files answered the same nextAfter twice (${after}); the listing is incomplete.`, {
|
|
34
|
+
code: 'INVALID_RESPONSE',
|
|
35
|
+
status: 0,
|
|
36
|
+
tool: 'list_project_files',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
after = page.nextAfter;
|
|
40
|
+
}
|
|
38
41
|
}
|
|
39
42
|
function walk(dir, root = dir) {
|
|
40
43
|
const out = [];
|
|
@@ -49,103 +52,140 @@ function walk(dir, root = dir) {
|
|
|
49
52
|
}
|
|
50
53
|
return out;
|
|
51
54
|
}
|
|
55
|
+
function shellQuote(value) {
|
|
56
|
+
return /^[A-Za-z0-9._\/-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
57
|
+
}
|
|
52
58
|
export const files = {
|
|
53
59
|
name: 'files',
|
|
54
60
|
summary: 'Project memory files: ls, cat, write, pull <dir>, push <dir>',
|
|
55
61
|
usage: 'martini files ls | cat <path> | write <path> <content|@file|-> [--append|--create-only] | pull <dir> | push <dir> [--force]',
|
|
62
|
+
details: "The project's memory files as a directory. pull writes every file plus .martini/files.json (path → updatedAt, sha256); push sends only changed files, each guarded by expectedUpdatedAt, and refuses one edited server-side since the pull (PROJECT_FILE_CHANGED, reported under skipped) unless --force. project.md and Instructions files are read-only during a run and come back as skipped. Nothing is ever deleted. cat prints the raw content, not JSON.",
|
|
63
|
+
positionals: [
|
|
64
|
+
{
|
|
65
|
+
name: 'ls | cat <path> | write <path> <content|@file|-> | pull <dir> | push <dir>',
|
|
66
|
+
description: 'The subcommand',
|
|
67
|
+
required: true,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
flags: [
|
|
71
|
+
{ flag: 'append', kind: 'boolean', description: 'write: append to the file instead of replacing it' },
|
|
72
|
+
{ flag: 'create-only', kind: 'boolean', description: 'write: fail when the file already exists' },
|
|
73
|
+
{
|
|
74
|
+
flag: 'force',
|
|
75
|
+
kind: 'boolean',
|
|
76
|
+
description: 'push: overwrite files changed server-side since the pull, and files that appeared meanwhile',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
examples: [
|
|
80
|
+
'martini files cat notes.md',
|
|
81
|
+
'martini files write notes.md @notes.md',
|
|
82
|
+
'martini files pull ./memory && martini files push ./memory',
|
|
83
|
+
],
|
|
56
84
|
async run(context) {
|
|
57
85
|
const [sub, ...rest] = context.positionals;
|
|
58
86
|
const projectId = requireProject(context);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
case 'cat': {
|
|
64
|
-
const path = rest[0];
|
|
65
|
-
if (!path)
|
|
66
|
-
throw new ArgumentError('martini files cat <path>');
|
|
67
|
-
const file = await context.client.callTool('read_project_file', { projectId, path });
|
|
68
|
-
return file.content;
|
|
69
|
-
}
|
|
70
|
-
case 'write': {
|
|
71
|
-
const path = rest[0];
|
|
72
|
-
if (!path)
|
|
73
|
-
throw new ArgumentError('martini files write <path> <content|@file|->');
|
|
74
|
-
const content = readContentArgument(rest[1]);
|
|
75
|
-
return context.client.callTool('write_project_file', {
|
|
76
|
-
projectId,
|
|
77
|
-
path,
|
|
78
|
-
content,
|
|
79
|
-
...(flagBoolean(context.flags, 'append') ? { append: true } : {}),
|
|
80
|
-
...(flagBoolean(context.flags, 'create-only') ? { createOnly: true } : {}),
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
case 'pull': {
|
|
84
|
-
const dir = resolve(context.cwd, rest[0] ?? '.');
|
|
85
|
-
const listed = await listFiles(context, projectId);
|
|
86
|
-
const manifest = { projectId, pulledAt: new Date(context.now()).toISOString(), files: {} };
|
|
87
|
-
for (const entry of listed) {
|
|
88
|
-
const file = await context.client.callTool('read_project_file', {
|
|
89
|
-
projectId,
|
|
90
|
-
path: entry.path,
|
|
91
|
-
});
|
|
92
|
-
const target = join(dir, ...entry.path.split('/'));
|
|
93
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
94
|
-
writeFileSync(target, file.content);
|
|
95
|
-
manifest.files[entry.path] = { updatedAt: file.updatedAt, sha256: sha256(file.content) };
|
|
87
|
+
try {
|
|
88
|
+
switch (sub) {
|
|
89
|
+
case 'ls': {
|
|
90
|
+
return { projectId, files: await listFiles(context, projectId) };
|
|
96
91
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const manifestPath = join(dir, FILES_MANIFEST);
|
|
104
|
-
if (!existsSync(manifestPath)) {
|
|
105
|
-
throw new ArgumentError(`${dir} was not pulled with martini files pull (no ${FILES_MANIFEST}); pull first so pushes can detect concurrent edits.`);
|
|
92
|
+
case 'cat': {
|
|
93
|
+
const path = rest[0];
|
|
94
|
+
if (!path)
|
|
95
|
+
throw new ArgumentError('martini files cat <path>');
|
|
96
|
+
const file = await context.client.callTool('read_project_file', { projectId, path });
|
|
97
|
+
return file.content;
|
|
106
98
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
case 'write': {
|
|
100
|
+
const path = rest[0];
|
|
101
|
+
if (!path)
|
|
102
|
+
throw new ArgumentError('martini files write <path> <content|@file|->');
|
|
103
|
+
const content = readContentArgument(rest[1]);
|
|
104
|
+
return context.client.callTool('write_project_file', {
|
|
105
|
+
projectId,
|
|
106
|
+
path,
|
|
107
|
+
content,
|
|
108
|
+
...(flagBoolean(context.flags, 'append') ? { append: true } : {}),
|
|
109
|
+
...(flagBoolean(context.flags, 'create-only') ? { createOnly: true } : {}),
|
|
110
|
+
});
|
|
110
111
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (!statSync(abs).isFile())
|
|
118
|
-
continue;
|
|
119
|
-
const content = readFileSync(abs, 'utf8');
|
|
120
|
-
const known = manifest.files[path];
|
|
121
|
-
if (known && known.sha256 === sha256(content)) {
|
|
122
|
-
unchanged.push(path);
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
try {
|
|
126
|
-
const written = await context.client.callTool('write_project_file', {
|
|
112
|
+
case 'pull': {
|
|
113
|
+
const dir = resolve(context.cwd, rest[0] ?? '.');
|
|
114
|
+
const listed = await listFiles(context, projectId);
|
|
115
|
+
const manifest = { projectId, pulledAt: new Date(context.now()).toISOString(), files: {} };
|
|
116
|
+
for (const entry of listed) {
|
|
117
|
+
const file = await context.client.callTool('read_project_file', {
|
|
127
118
|
projectId,
|
|
128
|
-
path,
|
|
129
|
-
content,
|
|
130
|
-
...(known && !force ? { expectedUpdatedAt: known.updatedAt } : {}),
|
|
119
|
+
path: entry.path,
|
|
131
120
|
});
|
|
132
|
-
|
|
133
|
-
|
|
121
|
+
const target = join(dir, ...entry.path.split('/'));
|
|
122
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
123
|
+
writeFileSync(target, file.content);
|
|
124
|
+
manifest.files[entry.path] = { updatedAt: file.updatedAt, sha256: sha256(file.content) };
|
|
125
|
+
}
|
|
126
|
+
mkdirSync(join(dir, '.martini'), { recursive: true });
|
|
127
|
+
writeFileSync(join(dir, FILES_MANIFEST), JSON.stringify(manifest, null, 2) + '\n');
|
|
128
|
+
return { projectId, dir, pulled: Object.keys(manifest.files).length, manifest: join(dir, FILES_MANIFEST) };
|
|
129
|
+
}
|
|
130
|
+
case 'push': {
|
|
131
|
+
const dir = resolve(context.cwd, rest[0] ?? '.');
|
|
132
|
+
const manifestPath = join(dir, FILES_MANIFEST);
|
|
133
|
+
if (!existsSync(manifestPath)) {
|
|
134
|
+
throw new ArgumentError(`${dir} was not pulled with martini files pull (no ${FILES_MANIFEST}); pull first so pushes can detect concurrent edits.`);
|
|
135
|
+
}
|
|
136
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
137
|
+
if (manifest.projectId !== projectId) {
|
|
138
|
+
throw new ArgumentError(`${dir} was pulled from project ${manifest.projectId}, not ${projectId}.`);
|
|
134
139
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
const force = flagBoolean(context.flags, 'force') ?? false;
|
|
141
|
+
const pushed = [];
|
|
142
|
+
const unchanged = [];
|
|
143
|
+
const skipped = [];
|
|
144
|
+
for (const path of walk(dir)) {
|
|
145
|
+
const abs = join(dir, ...path.split('/'));
|
|
146
|
+
if (!statSync(abs).isFile())
|
|
138
147
|
continue;
|
|
148
|
+
const content = readFileSync(abs, 'utf8');
|
|
149
|
+
const known = manifest.files[path];
|
|
150
|
+
if (known && known.sha256 === sha256(content)) {
|
|
151
|
+
unchanged.push(path);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const written = await context.client.callTool('write_project_file', {
|
|
156
|
+
projectId,
|
|
157
|
+
path,
|
|
158
|
+
content,
|
|
159
|
+
...(!force ? (known ? { expectedUpdatedAt: known.updatedAt } : { createOnly: true }) : {}),
|
|
160
|
+
});
|
|
161
|
+
manifest.files[path] = { updatedAt: written.updatedAt, sha256: sha256(content) };
|
|
162
|
+
pushed.push(path);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error instanceof CliApiError && error.status < 500) {
|
|
166
|
+
skipped.push({ path, code: error.code, error: error.message });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
139
170
|
}
|
|
140
|
-
throw error;
|
|
141
171
|
}
|
|
172
|
+
manifest.pulledAt = new Date(context.now()).toISOString();
|
|
173
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
174
|
+
return { projectId, dir, pushed, unchanged: unchanged.length, skipped };
|
|
142
175
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
176
|
+
default:
|
|
177
|
+
throw new ArgumentError('martini files <ls | cat | write | pull | push>');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (sub === 'cat' &&
|
|
182
|
+
rest[0]?.startsWith('tools/') &&
|
|
183
|
+
error instanceof CliApiError &&
|
|
184
|
+
(/No project file|(?:file.*not found|not found.*file)|invalid.*path|path.*invalid|^File paths are relative|^The file path (?:is empty|is longer than|has more than)/i.test(error.message) ||
|
|
185
|
+
/^(?:PROJECT_FILE_NOT_FOUND|FILE_NOT_FOUND|NOT_FOUND|INVALID_PATH|INVALID_PROJECT_FILE_PATH)$/.test(error.code))) {
|
|
186
|
+
error.message += `\nKit files are not project files. Read them from the workflow's tools library in two steps: martini get_workflow_authoring --project ${projectId} --workflow-id <workflow-id> --sections workflow (read baseline.toolBundle.sha256), then martini get_workflow_authoring --project ${projectId} --workflow-id <workflow-id> --file-path=${shellQuote(rest[0].replace(/^tools\//, ''))} --expected-bundle-sha256 <that sha> (the workflow id comes from martini list_saved_workflows --project ${projectId}).`;
|
|
146
187
|
}
|
|
147
|
-
|
|
148
|
-
throw new ArgumentError('martini files <ls | cat | write | pull | push>');
|
|
188
|
+
throw error;
|
|
149
189
|
}
|
|
150
190
|
},
|
|
151
191
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/commands/files.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/commands/files.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,70 +1,198 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
1
2
|
import { hostname } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
2
4
|
import { CliApiError } from '../api.js';
|
|
3
5
|
import { flagBoolean, flagString } from '../args.js';
|
|
4
|
-
import { removeProfile, saveProfile } from '../config.js';
|
|
6
|
+
import { readJsonFile, removeProfile, saveProfile, userConfigPath } from '../config.js';
|
|
5
7
|
import { detectHostAgent } from '../host.js';
|
|
8
|
+
import { timeoutMs, wantsWait } from './context.js';
|
|
6
9
|
export function defaultDeviceLabel(env = process.env) {
|
|
7
10
|
const host = detectHostAgent(env);
|
|
8
11
|
return `${hostname()}${host ? ` (${host})` : ''}`;
|
|
9
12
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
export function pendingLoginPath(env = process.env) {
|
|
14
|
+
return join(dirname(userConfigPath(env)), 'login-pending.json');
|
|
15
|
+
}
|
|
16
|
+
function readPendingLogin(env, apiUrl, now) {
|
|
17
|
+
const pending = readJsonFile(pendingLoginPath(env));
|
|
18
|
+
if (!pending || pending.apiUrl !== apiUrl || typeof pending.pollToken !== 'string')
|
|
19
|
+
return null;
|
|
20
|
+
if (new Date(pending.expiresAt).getTime() <= now) {
|
|
21
|
+
clearPendingLogin(env);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return pending;
|
|
25
|
+
}
|
|
26
|
+
function writePendingLogin(env, pending) {
|
|
27
|
+
const path = pendingLoginPath(env);
|
|
28
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
29
|
+
writeFileSync(path, JSON.stringify(pending, null, 2) + '\n', { mode: 0o600 });
|
|
30
|
+
}
|
|
31
|
+
function clearPendingLogin(env) {
|
|
32
|
+
const path = pendingLoginPath(env);
|
|
33
|
+
if (existsSync(path))
|
|
34
|
+
unlinkSync(path);
|
|
35
|
+
}
|
|
36
|
+
export const ACCOUNT_PROFILE_NAME = 'account';
|
|
37
|
+
export function profileNameFor(organizationName, organizationId, existing) {
|
|
38
|
+
if (organizationId === null) {
|
|
39
|
+
const profiles = existing?.profiles ?? {};
|
|
40
|
+
const reusable = (name) => !profiles[name] || profiles[name]?.scope === 'user';
|
|
41
|
+
if (reusable(ACCOUNT_PROFILE_NAME))
|
|
42
|
+
return ACCOUNT_PROFILE_NAME;
|
|
43
|
+
for (let n = 2;; n += 1) {
|
|
44
|
+
const candidate = `${ACCOUNT_PROFILE_NAME}-${n}`;
|
|
45
|
+
if (reusable(candidate))
|
|
46
|
+
return candidate;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const slug = (organizationName ?? '')
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
52
|
+
.replace(/^-+|-+$/g, '');
|
|
53
|
+
if (!slug)
|
|
54
|
+
return organizationId;
|
|
55
|
+
const taken = existing?.profiles?.[slug];
|
|
56
|
+
if (taken?.organizationId && taken.organizationId !== organizationId)
|
|
57
|
+
return `${slug}-${organizationId.slice(0, 8)}`;
|
|
58
|
+
return slug;
|
|
59
|
+
}
|
|
15
60
|
export const login = {
|
|
16
61
|
name: 'login',
|
|
17
62
|
summary: 'Sign in: pair this device through the browser and save a 90-day key',
|
|
18
|
-
usage: 'martini login [--device <label>] [--profile <name>] [--no-default]',
|
|
63
|
+
usage: 'martini login [--device <label>] [--profile <name>] [--no-default] [--no-wait] [--timeout <seconds>]',
|
|
64
|
+
details: 'Prints a URL and a code. Open the URL, enter the code, approve as you (every organization you belong to — one login for all your work; saved as the profile "account") or for one organization only (saved as a profile named after it); the first profile becomes the default. Tools run as you either way and the project you name decides who is billed; an organization key additionally reaches that organization\'s REST API and is listed in its Settings. The pairing is remembered until it expires (10 minutes), so running martini login again resumes it instead of starting over — after --no-wait, a --timeout, or a shell step that was killed while the person was still approving. An agent therefore runs martini login --no-wait, tells the person the code, and runs martini login again once they have approved.',
|
|
65
|
+
flags: [
|
|
66
|
+
{
|
|
67
|
+
flag: 'device',
|
|
68
|
+
kind: 'string',
|
|
69
|
+
value: '<label>',
|
|
70
|
+
description: 'How this device is listed under Settings → Martini API (default: the hostname, plus the host agent when known)',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
flag: 'profile',
|
|
74
|
+
kind: 'string',
|
|
75
|
+
value: '<name>',
|
|
76
|
+
description: "Save the key under this name instead of the organization's",
|
|
77
|
+
},
|
|
78
|
+
{ flag: 'default', kind: 'boolean', description: '--no-default keeps the current default profile' },
|
|
79
|
+
],
|
|
80
|
+
examples: [
|
|
81
|
+
'martini login',
|
|
82
|
+
'martini login --no-wait # print the code and return; run martini login again after approval',
|
|
83
|
+
'martini login --profile studio --no-default',
|
|
84
|
+
],
|
|
19
85
|
async run(context) {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
86
|
+
const env = context.env;
|
|
87
|
+
const apiUrl = context.config.apiUrl;
|
|
22
88
|
const stderr = context.output.stderr ?? (text => process.stderr.write(text));
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
89
|
+
let pending = readPendingLogin(env, apiUrl, context.now());
|
|
90
|
+
if (pending) {
|
|
91
|
+
stderr([
|
|
92
|
+
'',
|
|
93
|
+
` Resuming the pairing started ${pending.startedAt}:`,
|
|
94
|
+
` open ${pending.verificationUrl}`,
|
|
95
|
+
` and enter the code ${pending.code}`,
|
|
96
|
+
'',
|
|
97
|
+
].join('\n'));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const deviceLabel = flagString(context.flags, 'device') ?? defaultDeviceLabel(env);
|
|
101
|
+
const started = await context.client.startLogin(deviceLabel);
|
|
102
|
+
pending = {
|
|
103
|
+
apiUrl,
|
|
104
|
+
code: started.code,
|
|
105
|
+
pollToken: started.pollToken,
|
|
106
|
+
verificationUrl: started.verificationUrl,
|
|
107
|
+
expiresAt: started.expiresAt,
|
|
108
|
+
intervalSec: started.interval,
|
|
109
|
+
deviceLabel,
|
|
110
|
+
startedAt: new Date(context.now()).toISOString(),
|
|
111
|
+
};
|
|
112
|
+
writePendingLogin(env, pending);
|
|
113
|
+
stderr(['', ` Open ${started.verificationUrl}`, ` and enter the code ${started.code}`, ''].join('\n'));
|
|
114
|
+
}
|
|
115
|
+
if (!wantsWait(context, true)) {
|
|
116
|
+
return {
|
|
117
|
+
status: 'pending',
|
|
118
|
+
code: pending.code,
|
|
119
|
+
verificationUrl: pending.verificationUrl,
|
|
120
|
+
expiresAt: pending.expiresAt,
|
|
121
|
+
next: 'Approve the code in the browser, then run martini login again to collect the key.',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
stderr(` Waiting for approval (until ${pending.expiresAt}; if this is interrupted, martini login resumes it)…\n\n`);
|
|
125
|
+
const expiry = new Date(pending.expiresAt).getTime();
|
|
126
|
+
const budget = flagString(context.flags, 'timeout') === undefined ? Infinity : context.now() + timeoutMs(context);
|
|
127
|
+
const intervalMs = Math.max(1000, (pending.intervalSec || 3) * 1000);
|
|
32
128
|
for (;;) {
|
|
33
|
-
|
|
34
|
-
|
|
129
|
+
if (context.now() >= budget) {
|
|
130
|
+
throw new CliApiError('Still waiting for the login to be approved in the browser.', {
|
|
131
|
+
code: 'LOGIN_PENDING',
|
|
132
|
+
status: 0,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
await context.sleep(intervalMs);
|
|
136
|
+
const polled = await context.client.pollLogin(pending.pollToken);
|
|
35
137
|
if (polled.status === 'pending') {
|
|
36
|
-
if (context.now() >
|
|
138
|
+
if (context.now() > expiry) {
|
|
139
|
+
clearPendingLogin(env);
|
|
37
140
|
throw new CliApiError('The login request expired before it was approved.', {
|
|
38
141
|
code: 'LOGIN_EXPIRED',
|
|
39
142
|
status: 0,
|
|
40
143
|
});
|
|
144
|
+
}
|
|
41
145
|
continue;
|
|
42
146
|
}
|
|
43
|
-
if (polled.status === 'denied')
|
|
147
|
+
if (polled.status === 'denied') {
|
|
148
|
+
clearPendingLogin(env);
|
|
44
149
|
throw new CliApiError('The login was denied in the browser.', { code: 'LOGIN_DENIED', status: 0 });
|
|
45
|
-
|
|
150
|
+
}
|
|
151
|
+
if (polled.status === 'expired') {
|
|
152
|
+
clearPendingLogin(env);
|
|
46
153
|
throw new CliApiError('The login request expired. Run martini login again.', {
|
|
47
154
|
code: 'LOGIN_EXPIRED',
|
|
48
155
|
status: 0,
|
|
49
156
|
});
|
|
50
|
-
|
|
157
|
+
}
|
|
158
|
+
if (polled.status === 'claimed') {
|
|
159
|
+
clearPendingLogin(env);
|
|
51
160
|
throw new CliApiError('This login was already collected elsewhere. Run martini login again.', {
|
|
52
161
|
code: 'LOGIN_CLAIMED',
|
|
53
162
|
status: 0,
|
|
54
163
|
});
|
|
164
|
+
}
|
|
55
165
|
if (polled.status !== 'approved')
|
|
56
166
|
continue;
|
|
57
|
-
const
|
|
167
|
+
const scope = polled.scope ?? (polled.organizationId === null ? 'user' : 'organization');
|
|
168
|
+
const organizationId = scope === 'user' ? null : polled.organizationId;
|
|
169
|
+
const me = scope === 'organization'
|
|
170
|
+
? await context
|
|
171
|
+
.clientFor({ ...context.config, apiKey: polled.key })
|
|
172
|
+
.me()
|
|
173
|
+
.catch(() => null)
|
|
174
|
+
: null;
|
|
175
|
+
const organizationName = me?.organization?.name ?? null;
|
|
176
|
+
const profileName = flagString(context.flags, 'profile') ??
|
|
177
|
+
profileNameFor(organizationName, organizationId, readJsonFile(userConfigPath(env)));
|
|
58
178
|
const path = saveProfile(profileName, {
|
|
59
179
|
apiKey: polled.key,
|
|
60
|
-
apiUrl
|
|
61
|
-
|
|
180
|
+
apiUrl,
|
|
181
|
+
scope,
|
|
182
|
+
...(organizationId ? { organizationId } : {}),
|
|
183
|
+
...(organizationName ? { organizationName } : {}),
|
|
62
184
|
...(polled.expiresAt ? { expiresAt: polled.expiresAt } : {}),
|
|
63
|
-
}, { env
|
|
185
|
+
}, { env, makeDefault: flagBoolean(context.flags, 'default') ?? true });
|
|
186
|
+
clearPendingLogin(env);
|
|
187
|
+
if (scope === 'user') {
|
|
188
|
+
stderr(' This key acts in every organization you belong to. --project decides where work lands and who is billed; create_project makes a personal project unless --team-id is given.\n\n');
|
|
189
|
+
}
|
|
64
190
|
return {
|
|
65
191
|
ok: true,
|
|
66
192
|
profile: profileName,
|
|
67
|
-
|
|
193
|
+
scope,
|
|
194
|
+
organizationId,
|
|
195
|
+
organizationName,
|
|
68
196
|
keyId: polled.keyId,
|
|
69
197
|
expiresAt: polled.expiresAt,
|
|
70
198
|
savedTo: path,
|
|
@@ -76,6 +204,14 @@ export const logout = {
|
|
|
76
204
|
name: 'logout',
|
|
77
205
|
summary: 'Forget a saved profile (the key itself is revoked from Settings → Martini API)',
|
|
78
206
|
usage: 'martini logout [--profile <name>]',
|
|
207
|
+
flags: [
|
|
208
|
+
{
|
|
209
|
+
flag: 'profile',
|
|
210
|
+
kind: 'string',
|
|
211
|
+
value: '<name>',
|
|
212
|
+
description: 'The profile to forget (default: the active one)',
|
|
213
|
+
},
|
|
214
|
+
],
|
|
79
215
|
async run(context) {
|
|
80
216
|
const name = flagString(context.flags, 'profile');
|
|
81
217
|
if (!name) {
|
|
@@ -90,18 +226,23 @@ export const logout = {
|
|
|
90
226
|
};
|
|
91
227
|
export const whoami = {
|
|
92
228
|
name: 'whoami',
|
|
93
|
-
summary: 'Which
|
|
229
|
+
summary: 'Which key this CLI acts as — its scope, organization, and your memberships — and where the config came from',
|
|
94
230
|
usage: 'martini whoami',
|
|
231
|
+
details: 'Result: { scope, organization, organizations, key, apiVersion, apiUrl, projectId, canvasId, runId, configSource }. scope is "user" for an account key (organization is null; organizations lists every one you belong to) or "organization" for a key bound to one.',
|
|
95
232
|
async run(context) {
|
|
96
233
|
const me = await context.client.me();
|
|
97
234
|
return {
|
|
235
|
+
scope: me.scope,
|
|
98
236
|
organization: me.organization,
|
|
237
|
+
organizations: me.organizations,
|
|
99
238
|
key: me.key,
|
|
100
239
|
apiVersion: me.apiVersion,
|
|
101
240
|
apiUrl: context.config.apiUrl,
|
|
102
241
|
projectId: context.config.projectId ?? null,
|
|
103
242
|
canvasId: context.config.canvasId ?? null,
|
|
104
243
|
runId: context.config.runId ?? null,
|
|
244
|
+
executionId: context.config.executionId ?? null,
|
|
245
|
+
workerId: context.config.workerId ?? null,
|
|
105
246
|
configSource: context.config.source,
|
|
106
247
|
};
|
|
107
248
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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"}
|