impel-cli 0.20.57 → 0.20.58
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/RELEASE_NOTES.md +4 -0
- package/package.json +1 -1
- package/src/cli.js +7 -0
- package/src/commands/provisionVendorClis.js +92 -0
- package/src/commands/tasks.js +35 -3
- package/src/macSetup.js +28 -7
- package/src/platformSetup.js +4 -2
- package/src/windowsSetup.js +27 -3
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
# Release notes
|
|
2
|
+
## 0.20.58 — Provision vendor CLIs on managed enrollment
|
|
3
|
+
|
|
4
|
+
- Ensures freshly enrolled managed devices receive the reviewed Claude Code and Codex CLI binaries without requiring a personal PAT or interactive vendor login.
|
|
5
|
+
- Keeps provisioning isolated from personal Claude/Codex profiles and reports missing vendor runtimes for follow-up convergence.
|
|
2
6
|
|
|
3
7
|
## 0.20.57 — Live tenant model catalogs for `impel codex` (`impel models sync`)
|
|
4
8
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import { cmdUpdate } from "./commands/update.js";
|
|
|
22
22
|
import { cmdNuke } from "./commands/nuke.js";
|
|
23
23
|
import { cmdExperimental } from "./commands/experimental.js";
|
|
24
24
|
import { cmdConverge } from "./commands/converge.js";
|
|
25
|
+
import { cmdProvisionVendorClis } from "./commands/provisionVendorClis.js";
|
|
25
26
|
import { cmdRemote } from "./commands/remote.js";
|
|
26
27
|
import { cmdNative } from "./commands/native.js";
|
|
27
28
|
import { ensureMacDeveloperTools } from "./macDeveloperTools.js";
|
|
@@ -225,6 +226,12 @@ async function dispatch(cmd, rest) {
|
|
|
225
226
|
case "_converge":
|
|
226
227
|
return cmdConverge(rest);
|
|
227
228
|
|
|
229
|
+
// Hidden, unauthenticated vendor-CLI provisioning, called by the managed
|
|
230
|
+
// enrollment bootstrap. A managed device has no PAT, so it cannot reach
|
|
231
|
+
// `impel setup` — and without this nothing ever installs Claude Code.
|
|
232
|
+
case "_provision-vendor-clis":
|
|
233
|
+
return cmdProvisionVendorClis(rest);
|
|
234
|
+
|
|
228
235
|
// Hidden OS-shell launcher. Setup/update guarantees the app is ready.
|
|
229
236
|
case "_app-launch": {
|
|
230
237
|
const [target = "all", ...args] = rest;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// `impel _provision-vendor-clis` — put the reviewed vendor CLIs on the machine
|
|
2
|
+
// without authenticating.
|
|
3
|
+
//
|
|
4
|
+
// `impel setup` is the authenticated path: it needs a PAT, and it also writes
|
|
5
|
+
// this CLI's isolated Claude/Codex profiles. An enrolled managed device has
|
|
6
|
+
// neither. Its credential is the tenant credential the bootstrap wrote, and its
|
|
7
|
+
// profiles belong to the managed runtime (impel-apps), which supplies
|
|
8
|
+
// CLAUDE_CONFIG_DIR/CODEX_HOME, the gateway origin, and the credential at
|
|
9
|
+
// launch. The one thing the managed chain cannot do is put Claude Code on the
|
|
10
|
+
// machine — the managed `claude` package is Claude Desktop, and the runtime
|
|
11
|
+
// requires a separately installed vendor Claude Code CLI at the reviewed pin.
|
|
12
|
+
//
|
|
13
|
+
// So enrollment calls this: the same reviewed installers `impel setup` runs,
|
|
14
|
+
// the same version gate, and nothing else. It is intentionally quiet and it
|
|
15
|
+
// never throws — a device must finish enrolling even if a vendor endpoint is
|
|
16
|
+
// unreachable, and `impel setup`, `impel update`, and the next enrollment
|
|
17
|
+
// refresh all re-run the same installer.
|
|
18
|
+
import { parseFlags } from "../args.js";
|
|
19
|
+
import { describeCliFailure, preparePlatformClis } from "../platformSetup.js";
|
|
20
|
+
import { PINNED_VENDOR_CLI_VERSIONS } from "../vendorCliVersions.js";
|
|
21
|
+
|
|
22
|
+
const SUPPORTED_TOOLS = Object.freeze(["claude", "codex"]);
|
|
23
|
+
|
|
24
|
+
export function parseProvisionTools(value) {
|
|
25
|
+
if (value === undefined || value === true || value === null || value === "") return [...SUPPORTED_TOOLS];
|
|
26
|
+
const requested = String(value).split(",").map((tool) => tool.trim()).filter(Boolean);
|
|
27
|
+
const unsupported = requested.filter((tool) => !SUPPORTED_TOOLS.includes(tool));
|
|
28
|
+
if (unsupported.length) throw new Error(`unsupported vendor CLI: ${unsupported.join(", ")}`);
|
|
29
|
+
return [...new Set(requested)];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function cmdProvisionVendorClis(argv = [], overrides = {}) {
|
|
33
|
+
const io = {
|
|
34
|
+
platform: process.platform,
|
|
35
|
+
preparePlatformClis,
|
|
36
|
+
logger: console,
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
const { flags } = parseFlags(argv, {
|
|
40
|
+
tools: { type: "string" },
|
|
41
|
+
json: { type: "boolean" },
|
|
42
|
+
"skip-install": { type: "boolean" },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let installTools;
|
|
46
|
+
try {
|
|
47
|
+
installTools = parseProvisionTools(flags.tools);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
io.logger.error(`impel _provision-vendor-clis: ${error.message}`);
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let prepared;
|
|
55
|
+
try {
|
|
56
|
+
prepared = await io.preparePlatformClis({
|
|
57
|
+
platform: io.platform,
|
|
58
|
+
vendorClisOnly: true,
|
|
59
|
+
skipInstall: flags["skip-install"] === true,
|
|
60
|
+
installTools,
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Enrollment must not fail because a vendor endpoint did. Report and exit
|
|
64
|
+
// nonzero so a caller that cares can see it; the bootstrap ignores it.
|
|
65
|
+
const report = { ok: false, versions: PINNED_VENDOR_CLI_VERSIONS, error: String(error?.message || error) };
|
|
66
|
+
io.logger.error(flags.json ? JSON.stringify(report) : `impel _provision-vendor-clis: ${report.error}`);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return report;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const installed = Object.fromEntries(
|
|
72
|
+
installTools.map((tool) => [tool, prepared.binaries?.[tool] || null]),
|
|
73
|
+
);
|
|
74
|
+
const missing = installTools.filter((tool) => !installed[tool]);
|
|
75
|
+
const report = {
|
|
76
|
+
ok: missing.length === 0,
|
|
77
|
+
versions: Object.fromEntries(installTools.map((tool) => [tool, PINNED_VENDOR_CLI_VERSIONS[tool]])),
|
|
78
|
+
installed,
|
|
79
|
+
missing,
|
|
80
|
+
installAttempted: prepared.installAttempted === true,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (flags.json) io.logger.log(JSON.stringify(report));
|
|
84
|
+
else {
|
|
85
|
+
for (const tool of installTools) {
|
|
86
|
+
if (installed[tool]) io.logger.log(`${tool} ${PINNED_VENDOR_CLI_VERSIONS[tool]}: ${installed[tool]}`);
|
|
87
|
+
}
|
|
88
|
+
if (missing.length) io.logger.error(`impel _provision-vendor-clis: ${describeCliFailure(prepared, { commandName: "_provision-vendor-clis", isTTY: false, skipClis: flags["skip-install"] === true })}`);
|
|
89
|
+
}
|
|
90
|
+
if (!report.ok) process.exitCode = 1;
|
|
91
|
+
return report;
|
|
92
|
+
}
|
package/src/commands/tasks.js
CHANGED
|
@@ -27,6 +27,7 @@ const HELP = `impel tasks - CRUD tickets in Impel
|
|
|
27
27
|
Usage:
|
|
28
28
|
impel tasks list [--org <org>] [--scope visible|done|all] [--json]
|
|
29
29
|
impel tasks get <id> [--org <org>] [--json]
|
|
30
|
+
impel tasks projects [--org <org>] [--json]
|
|
30
31
|
impel tasks create --title <title> [--description <md> | --description-file <path>] [options] [--json]
|
|
31
32
|
impel tasks update <id> [options] [--json]
|
|
32
33
|
impel tasks delete <id> --yes [--org <org>] [--json]
|
|
@@ -49,6 +50,7 @@ Options:
|
|
|
49
50
|
--assignee <id|none> Assignee member id, or "none".
|
|
50
51
|
--assigned-to <id|none> Alias for --assignee.
|
|
51
52
|
--due <YYYY-MM-DD|empty> Due date, or empty string to clear.
|
|
53
|
+
--project <name|empty> Project name, or empty string to clear.
|
|
52
54
|
--json Print raw JSON response.
|
|
53
55
|
`;
|
|
54
56
|
|
|
@@ -74,6 +76,7 @@ function flagSpec() {
|
|
|
74
76
|
assignee: { type: "string" },
|
|
75
77
|
"assigned-to": { type: "string" },
|
|
76
78
|
due: { type: "string" },
|
|
79
|
+
project: { type: "string" },
|
|
77
80
|
yes: { type: "boolean" },
|
|
78
81
|
};
|
|
79
82
|
}
|
|
@@ -186,7 +189,22 @@ function descriptionFromFlags(flags) {
|
|
|
186
189
|
return inline;
|
|
187
190
|
}
|
|
188
191
|
|
|
189
|
-
function
|
|
192
|
+
function validateProject(value) {
|
|
193
|
+
if (value === undefined || value === "") return value;
|
|
194
|
+
if (value.length > 120) fail("impel tasks: project must be 120 characters or fewer.");
|
|
195
|
+
if (value.trim() !== value) {
|
|
196
|
+
fail("impel tasks: project cannot have leading or trailing whitespace.");
|
|
197
|
+
}
|
|
198
|
+
if ([...value].some((character) => {
|
|
199
|
+
const code = character.charCodeAt(0);
|
|
200
|
+
return code <= 31 || code === 127;
|
|
201
|
+
})) {
|
|
202
|
+
fail("impel tasks: project cannot contain control characters.");
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function taskMutationBody(flags, { requireTitle = false } = {}) {
|
|
190
208
|
const body = {};
|
|
191
209
|
if (flags.org !== undefined) body.orgId = flags.org;
|
|
192
210
|
|
|
@@ -219,6 +237,8 @@ function mutationBody(flags, { requireTitle = false } = {}) {
|
|
|
219
237
|
if (assignedTo !== undefined) body.assignedTo = assignedTo;
|
|
220
238
|
|
|
221
239
|
if (flags.due !== undefined) body.dueDate = flags.due;
|
|
240
|
+
const project = validateProject(flags.project);
|
|
241
|
+
if (project !== undefined) body.project = project;
|
|
222
242
|
|
|
223
243
|
return body;
|
|
224
244
|
}
|
|
@@ -262,6 +282,7 @@ function printTask(task, { orgId, markdown } = {}) {
|
|
|
262
282
|
console.log(`Priority: ${task.priority || "none"}`);
|
|
263
283
|
console.log(`Assignee: ${task.assignedTo || "unassigned"}`);
|
|
264
284
|
console.log(`Labels: ${(task.labels || []).join(", ") || "none"}`);
|
|
285
|
+
console.log(`Project: ${task.project || "none"}`);
|
|
265
286
|
if (task.latestRunId) console.log(`Run: ${task.latestRunId} (${task.runStatus || "unknown"})`);
|
|
266
287
|
if (markdown) {
|
|
267
288
|
console.log("");
|
|
@@ -294,6 +315,17 @@ export async function cmdTasks(argv) {
|
|
|
294
315
|
return;
|
|
295
316
|
}
|
|
296
317
|
|
|
318
|
+
case "projects": {
|
|
319
|
+
const payload = await requestJson({
|
|
320
|
+
flags,
|
|
321
|
+
path: "/api/cli/tasks/projects",
|
|
322
|
+
query: { orgId: flags.org },
|
|
323
|
+
});
|
|
324
|
+
if (flags.json) return printJson(payload);
|
|
325
|
+
for (const project of payload.projects || []) console.log(project);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
297
329
|
case "get": {
|
|
298
330
|
const issueId = positionals[0];
|
|
299
331
|
if (!issueId) fail("impel tasks get: missing task id.");
|
|
@@ -308,7 +340,7 @@ export async function cmdTasks(argv) {
|
|
|
308
340
|
}
|
|
309
341
|
|
|
310
342
|
case "create": {
|
|
311
|
-
const body =
|
|
343
|
+
const body = taskMutationBody(flags, { requireTitle: true });
|
|
312
344
|
const payload = await requestJson({
|
|
313
345
|
flags,
|
|
314
346
|
path: "/api/cli/tasks",
|
|
@@ -324,7 +356,7 @@ export async function cmdTasks(argv) {
|
|
|
324
356
|
case "update": {
|
|
325
357
|
const issueId = positionals[0];
|
|
326
358
|
if (!issueId) fail("impel tasks update: missing task id.");
|
|
327
|
-
const body =
|
|
359
|
+
const body = taskMutationBody(flags);
|
|
328
360
|
if (Object.keys(body).filter((key) => key !== "orgId").length === 0) {
|
|
329
361
|
fail("impel tasks update: pass at least one field to update.");
|
|
330
362
|
}
|
package/src/macSetup.js
CHANGED
|
@@ -26,7 +26,7 @@ export const MAC_CLI_INSTALLERS = Object.freeze({
|
|
|
26
26
|
"https://claude.ai",
|
|
27
27
|
"https://downloads.claude.ai",
|
|
28
28
|
]),
|
|
29
|
-
sha256: "
|
|
29
|
+
sha256: "3a68d3406cf674e17bed1733a4dcf37805e2e47d87417700007d7e1aa766a944",
|
|
30
30
|
command: `curl -fsSL https://claude.ai/install.sh | bash -s ${PINNED_VENDOR_CLI_VERSIONS.claude}`,
|
|
31
31
|
args: Object.freeze([PINNED_VENDOR_CLI_VERSIONS.claude]),
|
|
32
32
|
environment: Object.freeze({}),
|
|
@@ -177,6 +177,10 @@ export async function prepareMacClis({
|
|
|
177
177
|
credential = null,
|
|
178
178
|
skipInstall = false,
|
|
179
179
|
inspectOnly = false,
|
|
180
|
+
// Install the reviewed vendor binaries and stop there; see the same option
|
|
181
|
+
// on prepareWindowsClis for why an enrolled managed device wants the
|
|
182
|
+
// binaries without this CLI's isolated profiles.
|
|
183
|
+
vendorClisOnly = false,
|
|
180
184
|
installTools = ["claude", "codex"],
|
|
181
185
|
crossAppModels = false,
|
|
182
186
|
} = {}, dependencies = {}) {
|
|
@@ -245,6 +249,25 @@ export async function prepareMacClis({
|
|
|
245
249
|
const binaries = installAttempted
|
|
246
250
|
? detectMacClis(io.find, io.environment, io.verify)
|
|
247
251
|
: before;
|
|
252
|
+
const missingAfter = Object.entries(binaries).filter(([, binary]) => !binary).map(([tool]) => tool);
|
|
253
|
+
const installSucceeded = installAttempted
|
|
254
|
+
? Object.values(installations).every((installation) => installation.succeeded)
|
|
255
|
+
: null;
|
|
256
|
+
const installFailure = Object.values(installations).find((installation) => installation.failure)?.failure || null;
|
|
257
|
+
const installCommands = Object.fromEntries(missingBefore.map((tool) => [tool, io.installers[tool].command]));
|
|
258
|
+
if (vendorClisOnly) {
|
|
259
|
+
return {
|
|
260
|
+
binaries,
|
|
261
|
+
missingBefore,
|
|
262
|
+
missingAfter,
|
|
263
|
+
installAttempted,
|
|
264
|
+
installSucceeded,
|
|
265
|
+
installFailure,
|
|
266
|
+
installations,
|
|
267
|
+
installCommands,
|
|
268
|
+
profiles: null,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
248
271
|
const claudeProfile = io.ensureClaudeProfile(gatewayUrl, tenantId, { crossAppModels });
|
|
249
272
|
const codexProfile = io.ensureCodexProfile(gatewayUrl, tenantId, { crossAppModels });
|
|
250
273
|
if (binaries.claude) {
|
|
@@ -278,14 +301,12 @@ export async function prepareMacClis({
|
|
|
278
301
|
return {
|
|
279
302
|
binaries,
|
|
280
303
|
missingBefore,
|
|
281
|
-
missingAfter
|
|
304
|
+
missingAfter,
|
|
282
305
|
installAttempted,
|
|
283
|
-
installSucceeded
|
|
284
|
-
|
|
285
|
-
: null,
|
|
286
|
-
installFailure: Object.values(installations).find((installation) => installation.failure)?.failure || null,
|
|
306
|
+
installSucceeded,
|
|
307
|
+
installFailure,
|
|
287
308
|
installations,
|
|
288
|
-
installCommands
|
|
309
|
+
installCommands,
|
|
289
310
|
profiles: { claude: claudeProfile.configDir, codex: codexProfile.codexHome },
|
|
290
311
|
};
|
|
291
312
|
}
|
package/src/platformSetup.js
CHANGED
|
@@ -27,8 +27,10 @@ export async function preparePlatformClis(options = {}, dependencies = {}) {
|
|
|
27
27
|
installCommands: {},
|
|
28
28
|
profiles: null,
|
|
29
29
|
};
|
|
30
|
-
// Discovery-only probes must not write profiles either
|
|
31
|
-
|
|
30
|
+
// Discovery-only probes must not write profiles either, and neither does a
|
|
31
|
+
// vendor-CLI-only pass — which on this platform has no installer to run, so
|
|
32
|
+
// it degrades to the same discovery result.
|
|
33
|
+
if (options.inspectOnly || options.vendorClisOnly) return result;
|
|
32
34
|
const crossAppModels = options.crossAppModels === true;
|
|
33
35
|
const claudeProfile = (dependencies.ensureClaudeProfile || ensureImpelClaudeProfile)(options.gatewayUrl, options.tenantId, { crossAppModels });
|
|
34
36
|
const codexProfile = (dependencies.ensureCodexProfile || ensureImpelCodexProfile)(options.gatewayUrl, options.tenantId, { crossAppModels });
|
package/src/windowsSetup.js
CHANGED
|
@@ -142,6 +142,13 @@ export async function prepareWindowsClis({
|
|
|
142
142
|
credential = null,
|
|
143
143
|
skipInstall = false,
|
|
144
144
|
inspectOnly = false,
|
|
145
|
+
// Install the reviewed vendor binaries and stop there. An enrolled managed
|
|
146
|
+
// device has no PAT and never uses this CLI's isolated profiles: the managed
|
|
147
|
+
// runtime supplies CLAUDE_CONFIG_DIR/CODEX_HOME, the credential, and the
|
|
148
|
+
// gateway origin at launch. What it cannot do is put Claude Code on the
|
|
149
|
+
// machine, so enrollment reuses this installer for the binaries alone
|
|
150
|
+
// (`impel _provision-vendor-clis`) and writes no profile it would not read.
|
|
151
|
+
vendorClisOnly = false,
|
|
145
152
|
installTools = ["claude", "codex"],
|
|
146
153
|
crossAppModels = false,
|
|
147
154
|
} = {}, dependencies = {}) {
|
|
@@ -196,9 +203,10 @@ export async function prepareWindowsClis({
|
|
|
196
203
|
// target machines are brand new — provision the Impel-managed MinGit before
|
|
197
204
|
// any skill sync, on every setup/update pass (idempotent once installed).
|
|
198
205
|
// A failure degrades to the skill-sync "git is missing" skip, never a throw.
|
|
199
|
-
|
|
206
|
+
// A vendor-CLI-only pass syncs no skills, so it downloads no git either.
|
|
207
|
+
let gitBinary = vendorClisOnly ? null : io.find("git", io.environment, "win32");
|
|
200
208
|
let gitProvision = null;
|
|
201
|
-
if (!gitBinary) {
|
|
209
|
+
if (!vendorClisOnly && !gitBinary) {
|
|
202
210
|
gitProvision = await io.provisionGit({ environment: io.environment, logger: io.logger });
|
|
203
211
|
gitBinary = gitProvision?.installed
|
|
204
212
|
? gitProvision.binary
|
|
@@ -239,6 +247,22 @@ export async function prepareWindowsClis({
|
|
|
239
247
|
? detectWindowsClis(io.find, io.environment, io.verify)
|
|
240
248
|
: before;
|
|
241
249
|
const drifted = detectDriftedWindowsClis(io.find, io.environment, binaries, { versionOf: io.versionOf });
|
|
250
|
+
const missingAfter = Object.entries(binaries).filter(([, binary]) => !binary).map(([tool]) => tool);
|
|
251
|
+
if (vendorClisOnly) {
|
|
252
|
+
return {
|
|
253
|
+
binaries,
|
|
254
|
+
missingBefore,
|
|
255
|
+
missingAfter,
|
|
256
|
+
drifted,
|
|
257
|
+
installAttempted,
|
|
258
|
+
installSucceeded,
|
|
259
|
+
installFailure,
|
|
260
|
+
installations,
|
|
261
|
+
installCommands: windowsCliInstallCommands(missingBefore),
|
|
262
|
+
profiles: null,
|
|
263
|
+
git: { binary: null, provision: null },
|
|
264
|
+
};
|
|
265
|
+
}
|
|
242
266
|
const claudeProfile = io.ensureClaudeProfile(gatewayUrl, tenantId, { crossAppModels });
|
|
243
267
|
const codexProfile = io.ensureCodexProfile(gatewayUrl, tenantId, { crossAppModels });
|
|
244
268
|
|
|
@@ -267,7 +291,7 @@ export async function prepareWindowsClis({
|
|
|
267
291
|
return {
|
|
268
292
|
binaries,
|
|
269
293
|
missingBefore,
|
|
270
|
-
missingAfter
|
|
294
|
+
missingAfter,
|
|
271
295
|
drifted,
|
|
272
296
|
installAttempted,
|
|
273
297
|
installSucceeded,
|